Use the Notification API from your backend to send email and SMS notifications, check delivery status, list templates, manage recipient preferences, and receive delivery callbacks.
This is a server-to-server API. Do not expose API keys in browsers, mobile apps, client-side JavaScript, URLs, or public repositories.
Quick start
You need:
- An API base URL, such as
https://notifications.example.com/api/v1 - An API key supplied by your Notification Engine administrator
- An active template short code
Set these values in your backend secret manager:
NOTIFICATION_API_URL=https://notifications.example.com/api/v1
NOTIFICATION_API_KEY=ne_live_your_key
Verify the connection:
curl "${NOTIFICATION_API_URL}/connection" \
--header "Authorization: Bearer ${NOTIFICATION_API_KEY}" \
--header "Accept: application/json"
Send a notification:
curl --request POST "${NOTIFICATION_API_URL}/notifications/send" \
--header "Authorization: Bearer ${NOTIFICATION_API_KEY}" \
--header "Idempotency-Key: order-782-payment-received-v1" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"short_code": "payment_received",
"recipient": {
"external_id": "customer_123",
"name": "Ali Khan",
"email": "[email protected]",
"phone": "+923001234567"
},
"variables": {
"customer_name": "Ali Khan",
"amount": "2500",
"order_number": "782"
},
"channels": ["email", "sms"]
}'
The API returns 202 Accepted with a notification ID. Use that ID to check status.
API overview
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/connection |
Verify credentials and view usage |
GET |
/templates |
List active templates and required variables |
POST |
/notifications/send |
Send a notification |
GET |
/notifications/{notification_id} |
Check notification and channel status |
GET |
/preferences/{external_id} |
Get recipient preferences |
PUT |
/preferences/{external_id} |
Update recipient preferences |
All endpoints require API-key authentication. There is no portal-login API.
Authentication and headers
Use your API key as a Bearer token:
Authorization: Bearer ne_live_your_key
Accept: application/json
X-API-Key is also supported:
X-API-Key: ne_live_your_key
Do not send different values in both headers. The API returns 401 api_key_header_conflict when they conflict.
For JSON requests, also send:
Content-Type: application/json
Request IDs
You may provide a UUID or ULID:
X-Request-Id: 0190c89a-2fc0-7f22-bdc8-02c82d1ab321
Every API response contains X-Request-Id. Include it when reporting an integration problem.
Verify connection
GET /connection
Use this endpoint during setup and health checks. capabilities.channels lists only channels currently enabled for the platform and connected/activated for your tenant. sms appears when your tenant uses platform SMS (SNS/dry-run ready) or has active custom Twilio credentials. slack and web appear only after your administrator connects them.
Example response:
{
"connected": true,
"tenant": {
"id": "8dbddf53-442a-4f8f-a559-57211f338f02",
"name": "Example Company",
"status": "active",
"timezone": "Asia/Karachi"
},
"environment": "production",
"capabilities": {
"channels": ["email", "sms", "slack", "web"],
"webhook_callbacks": true,
"idempotency": true
},
"usage": {
"period": "2026-07",
"used": 318,
"limit": 1000,
"remaining": 682
}
}
List templates
GET /templates
The response lists active templates available to your account. Use short_code when sending and provide every key listed in variables.
Example response:
{
"data": [
{
"short_code": "payment_received",
"name": "Payment Received",
"category": "transactional",
"description": "Confirms a customer payment.",
"allowed_channels": ["email", "sms"],
"variables": [
"customer_name",
"amount",
"order_number"
],
"content": {
"email": { "has_html": true, "has_text": true },
"sms": { "configured": true, "max_length": 170 }
}
}
]
}
The current API returns all active templates. Filtering and pagination are not supported.
Every template now supports per-channel content (email HTML/text, SMS, web title/body, Slack) in
addition to the legacy subject/body pair; the send API request/response shape is unchanged.
The additive content object reports, for each allowed channel, whether content is configured and
the rendered length limit that applies — it never includes the actual template content. Absent keys
mean the channel is not in allowed_channels.
Send a notification
POST /notifications/send
Request
{
"short_code": "payment_received",
"recipient": {
"external_id": "customer_123",
"name": "Ali Khan",
"email": "[email protected]",
"phone": "+923001234567",
"slack_user_id": "U012ABCDEF",
"slack_channel_id": "C012ABCDEF",
"web_client_id": "customer_123",
"web_channel": "tenant:uuid:user:customer_123"
},
"variables": {
"customer_name": "Ali Khan",
"amount": "2500",
"order_number": "782"
},
"channels": ["email", "sms", "slack", "web"],
"callback_url": "https://customer.example.com/webhooks/notifications"
}
Fields
| Field | Required | Description |
|---|---|---|
short_code |
Yes | Active template short code |
recipient.external_id |
Yes | Your stable recipient ID, maximum 100 characters |
recipient.name |
No | Recipient name, maximum 150 characters |
recipient.email |
For email | Valid email address |
recipient.phone |
For SMS | E.164 number, such as +923001234567 |
recipient.slack_user_id |
For Slack when no channel/default | Slack member ID (U… / W…) |
recipient.slack_channel_id |
For Slack when no user/default | Slack channel ID (C… / G… / D…) |
recipient.web_client_id |
For web per tenant mode | Ably client ID |
recipient.web_channel |
For web per tenant mode | Ably channel name |
variables |
Yes | Values required by the selected template |
channels |
No | Subset of email, sms, slack, web (max 4); template defaults when omitted |
callback_url |
No | HTTPS endpoint for the final status callback |
metadata is not supported.
The effective channels are limited by platform enablement, tenant channel configuration, the template, requested channels, and recipient preferences. The API returns 422 no_channels_available if no channel remains. Platform-disabled channels return 422 channel_disabled. Unconfigured SMS (incomplete custom Twilio) / Slack / Web returns 422 channel_not_configured. Missing Slack/web destinations return 422 slack_destination_missing / 422 web_destination_missing.
SMS credentials are configured in the admin SMS Settings screen (platform SNS or custom Twilio). There is no public API to submit Twilio credentials per send.
Quota remains one monthly unit per accepted notification, regardless of how many channels are selected.
Idempotent retries
Send a stable Idempotency-Key for each business event:
Idempotency-Key: order-782-payment-received-v1
The key is optional, printable ASCII, and limited to 128 characters.
- Same key and same request: returns the original notification without using quota again
- Same key and different request: returns
409 idempotency_key_conflict - No key: every request creates a new notification
Accepted response
{
"message": "Notification accepted for processing.",
"notification_id": "e909f77f-a7d5-4c90-b401-33d6984f751b",
"status": "accepted",
"selected_channels": ["email", "sms"]
}
An accepted request is processed asynchronously. It does not mean the provider has sent it yet.
Check notification status
GET /notifications/{notification_id}
Example:
curl "${NOTIFICATION_API_URL}/notifications/e909f77f-a7d5-4c90-b401-33d6984f751b" \
--header "Authorization: Bearer ${NOTIFICATION_API_KEY}" \
--header "Accept: application/json"
Example response:
{
"notification_id": "e909f77f-a7d5-4c90-b401-33d6984f751b",
"short_code": "payment_received",
"status": "partial_failed",
"callback_status": "pending",
"recipient": {
"external_id": "customer_123",
"name": "Ali Khan",
"email": "[email protected]",
"phone": "+923001234567"
},
"requested_channels": ["email", "sms"],
"selected_channels": ["email", "sms"],
"deliveries": [
{
"channel": "email",
"status": "sent",
"provider_message_id": "safe-provider-id",
"attempt_count": 1,
"failure_reason": null,
"sent_at": "2026-07-22T00:15:04Z"
},
{
"channel": "sms",
"status": "failed",
"provider_message_id": null,
"attempt_count": 1,
"failure_reason": "The SMS destination was rejected.",
"sent_at": null
}
],
"accepted_at": "2026-07-22T00:15:00Z",
"finalized_at": "2026-07-22T00:15:05Z"
}
Status values
| Status | Meaning |
|---|---|
accepted |
Request stored and awaiting queue processing |
queued |
Delivery work queued |
sending |
At least one channel is processing |
sent |
All selected channels were accepted by their providers |
partial_failed |
Some channels sent and some failed |
failed |
All selected channels failed |
Terminal statuses are sent, partial_failed, and failed.
sent means the configured provider accepted the message. It does not guarantee recipient delivery or reading.
Recipient preferences
Preferences are stored against your external_id and a template.
Get preferences
GET /preferences/{external_id}
{
"external_id": "customer_123",
"preferences": [
{
"template_short_code": "payment_received",
"recipient_name": "Ali Khan",
"recipient_email": "[email protected]",
"recipient_phone": "+923001234567",
"recipient_slack_user_id": "U012ABCDEF",
"recipient_web_client_id": "customer_123",
"email_enabled": true,
"sms_enabled": false,
"slack_enabled": true,
"web_enabled": false,
"updated_at": "2026-07-22T00:00:00Z"
}
]
}
An empty preferences array means no overrides exist; template channels are enabled by default.
Update preferences
PUT /preferences/{external_id}
{
"preferences": [
{
"template_short_code": "payment_received",
"recipient_name": "Ali Khan",
"recipient_email": "[email protected]",
"recipient_phone": "+923001234567",
"email_enabled": true,
"sms_enabled": false,
"slack_enabled": true,
"web_enabled": false,
"recipient_slack_user_id": "U012ABCDEF",
"recipient_web_client_id": "customer_123"
}
]
}
The API atomically creates or updates the submitted template rows. Existing rows omitted from the request are retained.
Webhook callbacks
When callback_url is included in a send request, the engine sends one event type after the notification reaches a terminal status:
notification.finalized
Callbacks are delivered at least once. Deduplicate them using event and notification_id.
Callback headers
Content-Type: application/json
X-Notification-Event: notification.finalized
X-Notification-Timestamp: 1784679300
X-Notification-Signature: sha256=lowercase_hex_hmac
X-Notification-Id: e909f77f-a7d5-4c90-b401-33d6984f751b
X-Request-Id: 0190c89a-2fc0-7f22-bdc8-02c82d1ab321
Verify the signature
The signed value is:
{X-Notification-Timestamp}.{raw_request_body}
Calculate:
HMAC-SHA256(webhook_signing_secret, signed_value)
Then prefix the lowercase hex result with sha256=.
Your webhook should:
- Read the raw body before JSON parsing.
- Reject timestamps older or newer than five minutes.
- Compare signatures using a constant-time function.
- Return a
2xxresponse quickly. - Process the event asynchronously and idempotently.
X-Request-Id is for correlation and is not part of the signature.
Callback payload
{
"event": "notification.finalized",
"notification_id": "e909f77f-a7d5-4c90-b401-33d6984f751b",
"short_code": "payment_received",
"status": "partial_failed",
"recipient": {
"external_id": "customer_123",
"email": "[email protected]",
"phone": "+923001234567"
},
"deliveries": [
{
"channel": "email",
"status": "sent",
"provider_message_id": "safe-provider-id"
},
{
"channel": "sms",
"status": "failed",
"provider_message_id": null
}
],
"occurred_at": "2026-07-22T00:15:05Z"
}
Errors and retries
Errors use a stable JSON shape:
{
"code": "validation_failed",
"message": "The request data is invalid.",
"details": {
"fields": {
"recipient.email": [
"The recipient.email field must be a valid email address."
]
}
}
}
Common errors:
| HTTP | Code | Action |
|---|---|---|
401 |
api_key_required |
Add an authentication header |
401 |
api_key_invalid |
Check the key and environment |
401 |
api_key_expired |
Request a replacement key |
401 |
api_key_revoked |
Request a replacement key |
403 |
tenant_inactive / tenant_suspended |
Contact your administrator |
404 |
notification_not_found |
Check the notification ID and account |
404 |
template_not_found |
Refresh the template list |
409 |
idempotency_key_conflict |
Use the original payload or a new business key |
422 |
validation_failed |
Correct the fields in details.fields |
422 |
template_variables_invalid |
Match the template variable list |
422 |
template_render_failed |
Contact your administrator; the template failed safe rendering |
422 |
sms_body_too_long / web_title_too_long / web_body_too_long / slack_body_too_long |
The rendered channel content exceeds its configured limit; ask your administrator to shorten the template or variable values |
422 |
template_channel_content_missing |
A selected channel is allowed on the template but has no content configured; contact your administrator |
422 |
no_channels_available |
Check template channels and preferences |
422 |
channel_disabled |
Channel is disabled platform-wide; contact your administrator |
422 |
channel_not_configured |
Ask your administrator to configure SMS (platform or Twilio), or connect Slack/web for your account |
422 |
slack_destination_missing |
Provide recipient.slack_user_id or recipient.slack_channel_id, or set a tenant default |
422 |
web_destination_missing |
Provide recipient.web_client_id / recipient.web_channel matching the tenant publish mode |
422 |
callback_url_invalid / callback_url_forbidden |
Use a safe public HTTPS endpoint |
429 |
rate_limit_exceeded |
Wait and retry with backoff |
429 |
monthly_limit_exceeded |
Contact your administrator about quota |
500 |
internal_error |
Retry safely using the same idempotency key |
503 |
service_unavailable |
Retry safely using the same idempotency key |
Retry only temporary failures (429, 500, and 503) with exponential backoff and jitter. Reuse the same Idempotency-Key.
Do not retry validation, authentication, permission, or idempotency-conflict errors without correcting the request.
Go-live checklist
- Store the API key in a backend secret manager.
- Never expose the API key in frontend code.
- Verify
/connectionin each environment. - Fetch templates and validate required variables.
- Use a stable idempotency key for every business event.
- Store returned notification IDs.
- Poll status with bounded backoff or implement callbacks.
- Verify webhook signatures against the raw request body.
- Handle duplicate callbacks idempotently.
- Log
X-Request-Idfor support and troubleshooting.