REST API Reference
Direct HTTP integration for the Herald Notification Gateway.
REST API Reference
The Herald Notification Gateway provides a RESTful API for protocols that prefer direct HTTP calls over the SDK.
Authentication
All requests (except /health) must include a Bearer token in the Authorization header.
Authorization: Bearer hrld_live_your_key_hereNotifications
Send Notification
POST /v1/notify
Queues a notification for a specific wallet address.
Request Body:
{
"wallet": "7xR4mKp2nQ...",
"subject": "Liquidation Warning",
"body": "Your health factor dropped to 1.03.",
"category": "defi",
"priority": "critical",
"preferredChannel": "telegram",
"receipt": true,
"idempotencyKey": "liq_7xR4_1716000000000"
}| Field | Type | Required | Description |
|---|---|---|---|
wallet | string | Yes | Solana wallet address |
subject | string | Yes | Notification subject line |
body | string | Yes | Notification body (Markdown supported) |
category | string | Yes | defi | governance | system | marketing | security |
priority | string | No | normal | important | critical — critical adds SMS fallback |
preferredChannel | string | No | email | telegram | sms — hint only |
receipt | boolean | No | Write ZK proof of delivery on-chain (default false) |
templateId | string | No | Email template ID (Growth tier+) |
templateVariables | object | No | Variables to inject into the template |
idempotencyKey | string | No | Prevents duplicate sends |
Response (202 Accepted):
{
"notificationId": "01HWXYZ...",
"status": "queued",
"recipientRegistered": true,
"estimatedDeliveryMs": 3200,
"deliveryChannel": "telegram",
"environment": "production"
}| Field | Description |
|---|---|
status | queued | opted_out | duplicate | failed |
recipientRegistered | Whether the wallet is registered with Herald |
estimatedDeliveryMs | Estimated time to delivery in milliseconds |
deliveryChannel | Channel selected for delivery |
Bulk Notification
POST /v1/notify/bulk
Send up to 100 notifications in a single request.
Request Body:
{
"notifications": [
{
"wallet": "7xR4mKp2nQ...",
"subject": "Governance Proposal Live",
"body": "Vote on proposal #42 before it closes.",
"category": "governance",
"receipt": true,
"idempotencyKey": "prop42:7xR4mKp2nQ"
},
{
"wallet": "9yG2nRp4xQ...",
"subject": "Governance Proposal Live",
"body": "Vote on proposal #42 before it closes.",
"category": "governance",
"receipt": true,
"idempotencyKey": "prop42:9yG2nRp4xQ"
}
]
}Response (202 Accepted):
{
"results": [
{
"notificationId": "01HWXYZ...",
"status": "queued",
"recipientRegistered": true,
"estimatedDeliveryMs": 3200,
"deliveryChannel": "email"
},
{
"notificationId": "01HWYZA...",
"status": "opted_out",
"recipientRegistered": true,
"estimatedDeliveryMs": null,
"deliveryChannel": null
}
]
}Results are returned in the same order as the input notifications array.
Get Notification Status
GET /v1/notifications/:id
Retrieves the current delivery and receipt status of a notification.
Response (200 OK):
{
"notification_id": "01HWXYZ...",
"wallet": "7xR4mKp2nQ...",
"subject": "Liquidation Warning",
"status": "delivered",
"delivery_channel": "telegram",
"delivered_at": "2026-05-17T12:34:56Z",
"created_at": "2026-05-17T12:34:50Z",
"write_receipt": true,
"receipt_status": "confirmed",
"receipt_tx": "5xK9mPqR...",
"receipt_failure_reason": null,
"last_receipt_attempt_at": "2026-05-17T12:35:10Z"
}receipt_status values:
| Value | Meaning |
|---|---|
pending | Notification delivered — ZK receipt not yet written on-chain |
confirmed | Receipt landed on Solana (receipt_tx is populated) |
failed | On-chain write failed — see receipt_failure_reason |
disabled | Receipt was not requested for this notification |
List Notifications
GET /v1/notifications
Returns paginated notifications for the authenticated protocol.
Query Parameters:
page(optional, default1)limit(optional, default20, max100)
Response (200 OK):
{
"data": [
{
"notification_id": "01HWXYZ...",
"wallet": "7xR4mKp2nQ...",
"subject": "Liquidation Warning",
"status": "delivered",
"delivery_channel": "email",
"receipt_status": "confirmed",
"receipt_tx": "5xK9mP...",
"created_at": "2026-05-17T12:34:56Z"
}
],
"total": 142,
"page": 1
}Preview Notification
POST /v1/preview
Render how a notification will look before sending. Useful for template testing.
Request Body:
{
"wallet": "7xR4mKp2nQ...",
"subject": "Your weekly summary",
"body": "## Summary\n\nHere are your stats...",
"category": "defi",
"templateId": "tmpl_xxx"
}Response (200 OK):
{
"renderedHtml": "<html>...</html>",
"telegramText": "*Your weekly summary*\n\nHere are your stats...",
"smsText": "Your weekly summary: Here are your stats..."
}Schedules
Schedule One-Time Send
POST /v1/schedule
Request Body:
{
"wallet": "7xR4mKp2nQ...",
"subject": "Your staking rewards are ready",
"body": "You have 12.4 SOL in unclaimed staking rewards.",
"category": "defi",
"scheduledFor": "2026-06-01T09:00:00Z",
"timezone": "America/New_York"
}Response (201 Created):
{
"id": "sched_01H...",
"status": "PENDING",
"nextRunAt": "2026-06-01T13:00:00Z"
}Schedule Recurring (Cron)
POST /v1/schedule/cron
Request Body:
{
"wallet": "7xR4mKp2nQ...",
"subject": "Weekly portfolio summary",
"body": "Here is your weekly DeFi activity report.",
"category": "defi",
"cronExpr": "0 9 * * 1",
"timezone": "UTC"
}Response (201 Created):
{
"id": "sched_01H...",
"status": "PENDING",
"nextRunAt": "2026-05-26T09:00:00Z"
}List Schedules
GET /v1/schedule
Query Parameters: page, limit
Response (200 OK):
{
"items": [
{
"id": "sched_01H...",
"wallet": "7xR4mKp2nQ...",
"subject": "Weekly portfolio summary",
"status": "PENDING",
"nextRunAt": "2026-05-26T09:00:00Z"
}
],
"total": 3
}Cancel Schedule
DELETE /v1/schedule/:id
Response (200 OK):
{ "cancelled": true }Webhooks
Register Webhook
POST /v1/webhooks
Request Body:
{
"url": "https://myprotocol.com/webhooks/herald",
"events": ["notification.delivered", "notification.failed", "notification.bounced"]
}Response (201 Created):
{
"id": "wh_01HX...",
"url": "https://myprotocol.com/webhooks/herald",
"events": ["notification.delivered", "notification.failed", "notification.bounced"],
"isActive": true,
"secret": "whsec_xxx..."
}The secret is shown once — store it immediately.
List Webhooks
GET /v1/webhooks
Response (200 OK):
[
{
"id": "wh_01HX...",
"url": "https://myprotocol.com/webhooks/herald",
"events": ["notification.delivered"],
"isActive": true
}
]Update Webhook
PATCH /v1/webhooks/:id
Request Body:
{
"events": ["notification.delivered", "notification.bounced"],
"isActive": true
}Response (200 OK): Updated webhook object.
Test Webhook
POST /v1/webhooks/:id/test
Sends a synthetic notification.delivered event to verify connectivity.
Response (200 OK):
{ "sent": true }Delete Webhook
DELETE /v1/webhooks/:id
Response (204 No Content)
Email Templates
Custom templates require Growth tier or above.
Create Template
POST /v1/templates/email
Request Body:
{
"name": "Liquidation Alert",
"category": "defi",
"subjectTemplate": "Your {{protocol}} position is at risk",
"htmlSource": "<h1>Alert</h1><p>Health factor: {{healthFactor}}</p>",
"textSource": "Health factor: {{healthFactor}}",
"heraldFooter": "minimal"
}Response (201 Created):
{ "templateId": "tmpl_01HX..." }List Templates
GET /v1/templates/email
Response (200 OK):
{
"data": [
{
"id": "tmpl_01HX...",
"name": "Liquidation Alert",
"category": "defi",
"createdAt": "2026-05-01T00:00:00Z"
}
]
}Get Template
GET /v1/templates/email/:id
Update Template
PUT /v1/templates/email/:id
Creates a new version of the template.
Delete Template
DELETE /v1/templates/email/:id
Response (204 No Content)
Analytics
Delivery Analytics
GET /v1/analytics
Query Parameters:
period—7d|30d|90d(default30d)
Response (200 OK):
{
"period": "30d",
"total_sends": 4521,
"delivery_rate": 0.993,
"bounce_rate": 0.002,
"breakdown": {
"delivered": 4489,
"failed": 14,
"opted_out": 12,
"bounced": 6
}
}Engagement Metrics
GET /v1/engagement
Query Parameters: startDate, endDate (ISO 8601 dates)
Response (200 OK):
{
"openRate": 0.412,
"clickRate": 0.089,
"unsubscribeRate": 0.003
}Audience Insights
GET /v1/audience
Response (200 OK):
{
"totalRegistered": 18420,
"broadcastableSubscribers": 4200,
"channelCoverage": {
"email": 92,
"telegram": 41,
"sms": 12
},
"registrationTrend": [
{ "date": "2026-05-01", "count": 42 },
{ "date": "2026-05-02", "count": 67 }
]
}Protocol Info
GET /v1/protocols/me
Response (200 OK):
{
"id": "proto_01HX...",
"name": "My Protocol",
"tier": 1,
"tier_name": "Growth",
"sends_this_period": 12450,
"subscription_expires_at": "2026-06-01T00:00:00Z"
}tier | Name |
|---|---|
0 | Developer |
1 | Growth |
2 | Scale |
3 | Enterprise |
Usage
GET /v1/usage
Response (200 OK):
{
"tier": 1,
"usage": 12450,
"remaining": 37550,
"overageEnabled": false,
"resetsAt": "2026-06-01T00:00:00Z"
}API Request Log
GET /v1/requests
Query Parameters:
page(default1)limit(default50, max100)statusCode— filter by HTTP status (optional)endpoint— filter by path substring (optional)
Response (200 OK):
{
"items": [
{
"method": "POST",
"endpoint": "/v1/notify",
"statusCode": 202,
"latencyMs": 84,
"timestamp": "2026-05-17T12:34:56Z"
}
],
"total": 320,
"page": 1
}Subscriptions
Subscribe Wallet
POST /v1/subscriptions
{
"walletAddress": "7xR4mKp2nQ...",
"channels": ["email", "telegram"]
}Check Subscription
GET /v1/subscriptions/:wallet
{
"subscribed": true,
"channels": ["email"],
"subscribedAt": "2026-05-01T00:00:00Z"
}Unsubscribe Wallet
DELETE /v1/subscriptions/:wallet
Broadcast
POST /v1/broadcast
Requires Growth tier or above.
Request Body:
{
"subject": "Governance Vote: Protocol Upgrade v2",
"body": "A governance proposal is live. Vote before May 24.",
"category": "governance",
"receipt": false
}Response (202 Accepted):
{
"broadcast_id": "bcast_01H...",
"queued_count": 4200,
"total_subscribers": 4200,
"estimated_delivery_s": 120
}Health Check
GET /health
No authentication required.
{ "status": "ok", "timestamp": "2026-05-17T12:00:00Z" }Rate Limits
| Tier | Requests/sec | Monthly Sends |
|---|---|---|
| Developer | 2 | 1,000 |
| Growth | 20 | 50,000 |
| Scale | 100 | 250,000 |
| Enterprise | 500 | 1,000,000 |
Rate limit headers are included in every response:
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 19
X-RateLimit-Reset: 1716000060Error Responses
All errors follow a consistent shape:
{
"statusCode": 400,
"error": "Bad Request",
"message": "wallet is required",
"code": "VALIDATION_ERROR"
}Common error codes:
| Code | HTTP | Description |
|---|---|---|
INVALID_API_KEY | 401 | Missing or malformed API key |
INSUFFICIENT_QUOTA | 402 | Monthly send limit reached |
WALLET_NOT_REGISTERED | 404 | Wallet has no Herald identity |
DUPLICATE_IDEMPOTENCY_KEY | 409 | Notification already sent with this key |
RATE_LIMIT_EXCEEDED | 429 | Too many requests |
TEMPLATE_NOT_FOUND | 404 | Template ID does not exist |