Kalert
v1.0 REST / JSON

Kalert Messaging API

Async via Kafka · Multi-channel · Single endpoint

SMSEmailWhatsApp

The Kalert Messaging API lets you send messages across SMS, Email, and WhatsApp through a single unified endpoint. Messages are dispatched asynchronously via Kafka for fast acknowledgement and reliable delivery. All sends are logged to MongoDB for audit and reporting.

All requests must include a valid X-API-Key header. The API key determines the authenticated user, controls rate limiting, and validates sender permissions.

How it works

When you call POST /api/messages/send, the API validates your key, deducts credits, queues the message in Kafka, and returns a 202 QUEUED response immediately. The Kafka consumer picks up the message, dispatches it through the appropriate channel, and logs the outcome (SENT or FAILED). If dispatch fails, credits are automatically refunded.

Authentication

Every request must include your API key as a request header.

Header

X-API-Key: your-api-key-here
StatusBehaviour
active / enabledAccepted
disabledRejected with 401
Missing keyRejected with 401 Invalid API key
Never expose your API key in client-side code or public repositories.

Sender ID Endpoints (SMS)

Manage SMS Sender IDs. Each sender starts with pending status and must be approved before use.

Request a Sender ID

POST/api/senders/request
POST http://localhost:8080/api/senders/request
X-API-Key: your-api-key
Content-Type: application/json

{
  "senderName": "MySender",
  "purpose": "Transaction alerts"
}

Check Sender Status

GET/api/senders/{senderName}/status
GET http://localhost:8080/api/senders/MySender/status
X-API-Key: your-api-key

List Sender Records

GET/api/senders
GET http://localhost:8080/api/senders
X-API-Key: your-api-key

Errors & Response Codes

All error responses follow a consistent JSON structure:

{
  "status":    401,
  "message":   "Invalid API key",
  "timestamp": "2026-03-27T10:00:00Z"
}
CodeMeaningCommon cause
202AcceptedMessage queued successfully
200OKLog query returned successfully
400Bad requestMissing or invalid request body fields
401UnauthorizedInvalid, missing, or disabled API key; unapproved sender
402Payment requiredInsufficient credits
429Too many requestsRate limit of 100 req/min exceeded
500Server errorUnexpected internal error

Rate Limiting

Each API key is limited to 100 requests per minute. The bucket refills fully every 60 seconds.

When the limit is exceeded you receive a 429 response with a retry-after value in seconds.
{
  "status":    429,
  "message":   "Rate limit exceeded. Retry after 43s",
  "timestamp": "2026-03-27T10:00:00Z"
}

Credits & Billing

Credits are deducted immediately when a message is queued. Failed deliveries are automatically refunded.

ChannelRateNotes
SMS1 credit per 160-char segmentLong messages split into segments, billed per segment
WHATSAPP2 credits per messageEach send costs 2 credits regardless of length
EMAIL1 credit per emailSingle email to one recipient is always 1 credit
If delivery fails for any reason, the exact credits deducted are automatically refunded.
If all credit pools are insufficient the request returns 402 Payment Required and no message is queued.

Send a Message

The primary send endpoint. Validates your API key, deducts credits, publishes to Kafka, and returns immediately. Actual delivery happens asynchronously.

POST/api/messages/send

Common request fields

FieldTypeDescription
channelstringSMS, EMAIL, or WHATSAPP
recipientsarrayList of phone numbers or email addresses
senderNamestringApproved SMS Sender ID (SMS only)
senderEmailstringVerified sender email address (EMAIL only)
senderDisplayNamestringFriendly name shown next to the From address (EMAIL only)
bodystringPlain-text content (SMS, WhatsApp, and email fallback)
subjectstringEmail subject line (EMAIL only)
htmlBodystringHTML email content (EMAIL only)
templateIdnumberOptional. Reference a saved email template instead of sending subject/htmlBody/body inline. Convenience only — inline HTML works fine (EMAIL only)
templateVariablesobjectGlobal Handlebars variables applied to all recipients (EMAIL only)
recipientVariablesobjectPer-recipient variable overrides keyed by address (EMAIL only)
applyBrandingbooleanOptional. true wraps the outgoing HTML with your saved brand header/footer. Omit or false sends raw HTML as-is (default for API calls, so branding is never applied unexpectedly) (EMAIL only)
isSchedule / scheduleDatebool / stringSet on POST /api/schedule/send for future delivery (see Scheduled Messaging)

Bulk Sending

Provide a recipients array to send to multiple addresses in a single call. Credits are deducted per recipient at queue time and refunded per-recipient on failure.

For per-recipient personalization, add a recipientVariables object. Each key must be the exact phone number or email from your recipients array. The API uses this mapping to inject the right variables into each message.

POST http://localhost:8080/api/messages/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":    "SMS",
  "senderName": "YourSenderID",
  "recipients": ["+233244000001", "+233244000002"],
  "body":       "Hello {{name}}, your balance is GHS {{balance}}.",
  "recipientVariables": {
    "+233244000001": { "name": "First recipient name",  "balance": "120.00" },
    "+233244000002": { "name": "Second recipient name", "balance": "85.50"  }
  }
}
Key rule: every key in recipientVariables must exactly match a number in recipients. If a recipient has no entry in recipientVariables, their message uses the global templateVariables values (or an empty string if the variable is also missing there).

Response

{
  "status":    "QUEUED",
  "messageId": "a3f8c1d2-4e5b-4c6f-8d7e-9a0b1c2d3e4f"
}

Scheduled Messaging

Use POST /api/schedule/send to queue a message for future delivery. Set isSchedule to true and provide a scheduleDate. The message is stored and dispatched at the requested time.

POST http://localhost:8080/api/schedule/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":      "SMS",
  "senderName":   "Kalert",
  "recipients":   ["+233244000000"],
  "body":         "Your appointment is tomorrow at 9am.",
  "isSchedule":   true,
  "scheduleDate": "2026-09-01T09:00:00Z"
}
For recurring schedules, add an optional recursion value on the same endpoint (daily, weekly, monthly, or one of the other recognised cadences).

SMS

The senderName field must be an approved Sender ID linked to your account. Sender IDs are max 11 characters. Request one via POST /api/senders/request.

Basic SMS send

POST http://localhost:8080/api/messages/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":    "SMS",
  "senderName": "Kalert",
  "recipients": ["+233244000000"],
  "body":       "Your verification code is 482910. Valid for 5 minutes."
}

SMS with per-recipient personalization

Replace the phone numbers and variable values with your own. Each key in recipientVariables must exactly match an entry in recipients.

{
  "channel":    "SMS",
  "senderName": "YourSenderID",
  "recipients": ["+233244000001", "+233244000002"],
  "body":       "Hi {{name}}, your OTP is {{otp}}. Valid for 5 minutes.",
  "recipientVariables": {
    "+233244000001": { "name": "Recipient 1 name", "otp": "481234" },
    "+233244000002": { "name": "Recipient 2 name", "otp": "982710" }
  }
}

Response

{
  "status":    "QUEUED",
  "messageId": "a3f8c1d2-4e5b-4c6f-8d7e-9a0b1c2d3e4f"
}
Messages over 160 characters are automatically split into segments. Each segment costs 1 credit.

Email

The email channel supports HTML and plain-text content, Handlebars templates with per-recipient variables, file attachments, custom sender domains with DKIM signing, one-click unsubscribe headers, and full delivery event webhooks (delivered, opened, clicked, bounced, complained).

1 credit

per email delivered

Verified sender

email address or custom domain required

Handlebars

variables per-recipient supported

Sender verification

Before sending email you must verify either an email address or a custom domain. Domains get DKIM CNAME records for authentication.

Add a sender identity

POST/api/email/senders

Send one request with the identity you want to verify. Pass a single email address to verify just that address, or a bare domain to verify the entire domain (recommended — one setup covers every address on it).

POST http://localhost:8080/api/email/senders
X-API-Key: your-api-key
Content-Type: application/json

{
  "identity": "yourdomain.com"
}

To verify a single address instead of a domain, use "identity": "info@yourdomain.com".

Response

{
  "id":         12,
  "identity":   "yourdomain.com",
  "kind":       "DOMAIN",
  "status":     "PENDING",
  "provider":   "SES",
  "createdAt":  "2026-08-10T12:00:00Z",
  "verifiedAt": null,
  "dnsRecords": [
    {
      "name":  "_dkim1._domainkey.yourdomain.com",
      "type":  "CNAME",
      "value": "dkim1.amazonses.com"
    }
  ]
}
For domain verification, add the returned CNAME records to your DNS and wait for propagation (usually under 1 hour). SES checks automatically.

List sender identities

GET/api/email/senders
GET http://localhost:8080/api/email/senders
X-API-Key: your-api-key

Trigger re-verification

POST/api/email/senders/{id}/verify
POST http://localhost:8080/api/email/senders/12/verify
X-API-Key: your-api-key

Remove a sender identity

DELETE/api/email/senders/{id}
DELETE http://localhost:8080/api/email/senders/12
X-API-Key: your-api-key

Advanced send

Body vs. HTML body

Every send needs message content. You choose how to provide it:

  • htmlBody alone is fully supported — you do NOT need to also send body. Recipients see the rendered HTML in every modern mail client.
  • body alone sends a plain-text-only email. Simple and safe, but no formatting.
  • Both together is best practice for cold sends: modern clients render htmlBody; text-only clients and some spam filters read body as the fallback / verification. Improves deliverability.

Optional: instead of sending body/htmlBody inline, you can reference a saved template with templateId (see below). This is a convenience for reusing the same design across many sends. Most integrations just use htmlBody directly.

Basic HTML email

POST http://localhost:8080/api/messages/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":           "EMAIL",
  "senderEmail":       "info@yourdomain.com",
  "senderDisplayName": "Your Company",
  "recipients":        ["customer@example.com"],
  "subject":           "Your invoice is ready",
  "htmlBody":          "<h1>Invoice #INV-1042</h1><p>Amount due: <strong>GHS 250.00</strong></p>",
  "body":              "Invoice #INV-1042 - Amount due: GHS 250.00"
}

Bulk send with per-recipient personalization

Each address gets its own set of variable values. Use {{placeholders}} anywhere in htmlBody or body; they are rendered separately for each recipient.

{
  "channel":     "EMAIL",
  "senderEmail": "info@yourdomain.com",
  "recipients":  ["alice@yourdomain.com", "bob@yourdomain.com"],
  "subject":     "Your order update",
  "htmlBody":    "<p>Hi {{name}}, your order {{orderId}} has shipped.</p>",
  "recipientVariables": {
    "alice@yourdomain.com": { "name": "Alice", "orderId": "ORD-001" },
    "bob@yourdomain.com":   { "name": "Bob",   "orderId": "ORD-002" }
  }
}
Key rule: every key in recipientVariables must exactly match an address in recipients, including case and spacing. Entries that don't match any recipient are silently ignored; recipients with no entry fall back to the global templateVariables object.

Email with attachments

Provide a publicly accessible URL. The API fetches the file and attaches it automatically. Any file type is supported: PDFs, images, Word documents, spreadsheets, ZIPs, and more. The MIME type is detected automatically from the file contents.

{
  "channel":     "EMAIL",
  "senderEmail": "info@yourdomain.com",
  "recipients":  ["customer@example.com"],
  "subject":     "Your invoice (attached)",
  "htmlBody":    "<p>Please find your invoice attached.</p>",
  "attachments": [
    {
      "filename":   "invoice-1042.pdf",
      "contentUrl": "https://yourdomain.com/files/invoice-1042.pdf"
    },
    {
      "filename":   "report.xlsx",
      "contentUrl": "https://yourdomain.com/files/report.xlsx"
    },
    {
      "filename":   "banner.png",
      "contentUrl": "https://yourdomain.com/images/banner.png"
    }
  ]
}

Apply your saved brand header/footer

applyBranding is optional. Omit it or set false (the default for API calls) and Kalert sends your raw HTML as-is. Set true to wrap the outgoing HTML with the brand header/footer you configured in the dashboard — useful if you want a consistent look across sends without duplicating markup.

{
  "channel":       "EMAIL",
  "senderEmail":   "info@yourdomain.com",
  "recipients":    ["customer@example.com"],
  "subject":       "Welcome to Acme",
  "htmlBody":      "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
  "applyBranding": true
}
Branding is never applied by default on API sends — this prevents unexpected markup from appearing in transactional emails. Opt in with applyBranding: true only when you want it.

Optional: reuse a saved template

If you have designs you send often, you can save them as templates in the dashboard and reference them by templateId instead of sending htmlBody inline. This is purely a convenience — everything above works fine without it. When you do use a template, variables inside it ({{name}}, etc.) are replaced at send time using the values you provide.

{
  "channel":     "EMAIL",
  "senderEmail": "info@yourdomain.com",
  "recipients":  ["customer@yourdomain.com"],
  "templateId":  5,
  "templateVariables": {
    "name":    "Recipient name",
    "orderId": "ORD-XXXX",
    "amount":  "GHS 199.00"
  }
}
templateVariables applies the same values to all recipients. For different values per recipient, use recipientVariables the same way it works with inline htmlBody.

Scheduled email

Post to /api/schedule/send with isSchedule: true and a scheduleDate. Any of the payload shapes above (inline htmlBody, attachments, or a saved template) work here.

POST http://localhost:8080/api/schedule/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":      "EMAIL",
  "senderEmail":  "info@yourdomain.com",
  "recipients":   ["customer@example.com"],
  "subject":      "Your weekly report",
  "htmlBody":     "<h1>Your weekly report</h1><p>View attached details.</p>",
  "body":         "Your weekly report is ready — view attached details.",
  "isSchedule":   true,
  "scheduleDate": "2026-09-01T08:00:00Z"
}

Response (all send variants)

HTTP/1.1 202 Accepted

{
  "status":    "QUEUED",
  "messageId": "b7e2d4a9-1c3f-4b8e-9d6a-2f5c8e1a4b7d"
}

Template management

Create and manage reusable email templates. Templates support Handlebars syntax for dynamic content.

List templates

GET/api/email/templates
GET http://localhost:8080/api/email/templates
X-API-Key: your-api-key

Create a template

POST/api/email/templates
POST http://localhost:8080/api/email/templates
X-API-Key: your-api-key
Content-Type: application/json

{
  "name":     "Invoice Notification",
  "subject":  "Invoice #{{invoiceId}} is ready",
  "htmlBody": "<h1>Hi {{name}}</h1><p>Invoice #{{invoiceId}} - Amount: {{amount}}</p>",
  "textBody": "Hi {{name}}, Invoice #{{invoiceId}} - Amount: {{amount}}",
  "category": "TRANSACTIONAL"
}

Update a template

PUT/api/email/templates/{id}
PUT http://localhost:8080/api/email/templates/5
X-API-Key: your-api-key
Content-Type: application/json

{
  "subject":  "Updated subject - #{{invoiceId}}",
  "htmlBody": "<h1>Updated template content</h1>"
}

Delete a template

DELETE/api/email/templates/{id}
DELETE http://localhost:8080/api/email/templates/5
X-API-Key: your-api-key

Suppression list

Suppressed addresses are automatically skipped on future sends. Addresses are added automatically on hard bounces and spam complaints. You can also manage the list manually.

List suppressed addresses

GET/api/email/suppressions
GET http://localhost:8080/api/email/suppressions
X-API-Key: your-api-key

Response

[
  {
    "email":     "bounced@example.com",
    "reason":    "BOUNCE",
    "createdAt": "2026-08-10T12:00:00Z"
  }
]

Add an address to suppression

POST/api/email/suppressions
POST http://localhost:8080/api/email/suppressions
X-API-Key: your-api-key
Content-Type: application/json

{
  "email":  "user@example.com",
  "reason": "MANUAL"
}

Remove from suppression

DELETE/api/email/suppressions/{email}
DELETE http://localhost:8080/api/email/suppressions/user@example.com
X-API-Key: your-api-key

Webhook events

Register a URL and Kalert will POST delivery events to it in real time. Events include delivered, opened, clicked, bounced, complained, and failed.

Register a webhook

POST/api/webhooks
POST http://localhost:8080/api/webhooks
X-API-Key: your-api-key
Content-Type: application/json

{
  "url":    "https://yourserver.com/kalert/events",
  "events": "Delivery,Open,Click,Bounce,Complaint,Failed"
}

List webhooks

GET/api/webhooks
GET http://localhost:8080/api/webhooks
X-API-Key: your-api-key

Test a webhook

POST/api/webhooks/{id}/test
POST http://localhost:8080/api/webhooks/3/test
X-API-Key: your-api-key

Event payload (example: DELIVERED)

{
  "event":     "DELIVERED",
  "messageId": "b7e2d4a9-1c3f-4b8e-9d6a-2f5c8e1a4b7d",
  "channel":   "EMAIL",
  "recipient": "customer@example.com",
  "timestamp": "2026-08-10T12:05:23Z"
}

Available event types

EventMeaningAction taken
DELIVEREDAccepted by recipient serverNone
OPENEDRecipient opened the emailNone
CLICKEDRecipient clicked a linkNone
BOUNCEDHard bounce (invalid address)Address added to suppression; credit refunded
COMPLAINEDSpam complaint filedAddress added to suppression
FAILEDDispatch error (all retries exhausted)Credit refunded

WhatsApp

Send text, image, and document messages to WhatsApp users via the Meta Cloud API. Supports bulk personalized sends, approved template messages, inbound message replies, and opt-out management.

2 credits

per WhatsApp message sent

Registered number

WABA number linked to your account is used automatically

Handlebars

variables per-recipient supported

Recipients must have WhatsApp installed. In development/sandbox mode, recipients must first send a join code to the sandbox number before they can receive messages.

Send messages

Basic text message

POST http://localhost:8080/api/messages/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":    "WHATSAPP",
  "recipients": ["+233244000000"],
  "body":       "Hi! Your order has been shipped and will arrive tomorrow."
}

Response

HTTP/1.1 202 Accepted

{
  "status":    "QUEUED",
  "messageId": "c9f1e3b6-5d7a-4c2e-8b4f-6a3d9e2c5f8a"
}

Bulk and personalized sends

Send to multiple recipients in one call. Use templateVariables for shared content or recipientVariables for per-recipient personalization.

Replace the phone numbers and values below with your own. Each key in recipientVariables must exactly match a number in your recipients array.

POST http://localhost:8080/api/messages/send
X-API-Key: your-api-key
Content-Type: application/json

{
  "channel":    "WHATSAPP",
  "recipients": ["+233244000001", "+233244000002"],
  "body":       "Hi {{name}}, order #{{orderId}} is ready for pickup.",
  "recipientVariables": {
    "+233244000001": { "name": "Recipient 1 name", "orderId": "ORD-XXXX" },
    "+233244000002": { "name": "Recipient 2 name", "orderId": "ORD-YYYY" }
  }
}
Use {{variable}} in your message text. Values in recipientVariables override any matching key in the global templateVariables object for that specific recipient.

Media messages

Send images, documents, audio, or video by providing a publicly accessible URL in waMediaUrl, together with a waMediaType of image, document, audio, video, or sticker. Add an optional waMediaCaption for image, document, or video parts. Supported formats include images (JPEG, PNG, WebP), documents (PDF, DOCX, XLSX), audio (MP3, OGG), and video (MP4).

Image message

{
  "channel":        "WHATSAPP",
  "recipients":     ["+233244000000"],
  "waMediaType":    "image",
  "waMediaUrl":     "https://yourdomain.com/images/catalog.jpg",
  "waMediaCaption": "Check out our latest collection!"
}

Document message

{
  "channel":        "WHATSAPP",
  "recipients":     ["+233244000000"],
  "waMediaType":    "document",
  "waMediaUrl":     "https://yourdomain.com/files/invoice-1042.pdf",
  "waMediaCaption": "Your invoice is attached."
}

Template management

WhatsApp Business templates must be approved by Meta before use. Use the sync endpoint to pull approved templates from your Meta account into Kalert.

List templates

GET/api/whatsapp/templates
GET http://localhost:8080/api/whatsapp/templates
X-API-Key: your-api-key

Response

[
  {
    "id":       1,
    "name":     "order_confirmation",
    "language": "en",
    "status":   "APPROVED",
    "category": "TRANSACTIONAL"
  }
]

Sync templates from Meta

POST/api/whatsapp/templates/sync
POST http://localhost:8080/api/whatsapp/templates/sync
X-API-Key: your-api-key
Sync fetches the latest approved templates from your Meta WABA and updates the local database. Run this whenever you add or modify templates in Meta Business Manager.

Opt-outs and inbox

List opt-outs

GET/api/whatsapp/opt-outs
GET http://localhost:8080/api/whatsapp/opt-outs
X-API-Key: your-api-key

Add an opt-out

POST/api/whatsapp/opt-outs
POST http://localhost:8080/api/whatsapp/opt-outs
X-API-Key: your-api-key
Content-Type: application/json

{ "phone": "+233244000000" }

Remove an opt-out

DELETE/api/whatsapp/opt-outs/{phone}
DELETE http://localhost:8080/api/whatsapp/opt-outs/%2B233244000000
X-API-Key: your-api-key

Inbound messages (inbox)

View messages sent by users to your WhatsApp number and reply to them.

GET/api/whatsapp/inbox
GET http://localhost:8080/api/whatsapp/inbox
X-API-Key: your-api-key

Reply to an inbound message

POST/api/whatsapp/inbox/reply
POST http://localhost:8080/api/whatsapp/inbox/reply
X-API-Key: your-api-key
Content-Type: application/json

{
  "to":   "+233244000000",
  "body": "Thanks for reaching out! Your request has been received."
}

Webhook events

Register webhooks via POST /api/webhooks (same as email). WhatsApp fires the following events:

EventMeaningAction
DELIVEREDMessage delivered to deviceNone
READRecipient opened the messageNone
FAILEDDelivery failed (invalid number, no WA account)Credit refunded

Webhook payload example

{
  "event":     "DELIVERED",
  "messageId": "c9f1e3b6-5d7a-4c2e-8b4f-6a3d9e2c5f8a",
  "channel":   "WHATSAPP",
  "recipient": "+233244000000",
  "timestamp": "2026-08-10T12:08:47Z"
}

Error scenarios

401: Invalid or missing API key

{ "status": 401, "message": "Invalid API key", "timestamp": "..." }

401: API key disabled

{ "status": 401, "message": "API key is disabled", "timestamp": "..." }

401: Unapproved or unknown sender (SMS)

{ "status": 401, "message": "Sender 'MySender' not found, not owned by this user, or not approved", "timestamp": "..." }

402: Insufficient credits

{ "status": 402, "message": "Insufficient credits. Need 1 units but balance is insufficient.", "timestamp": "..." }

400: Validation error

{
  "status":  400,
  "message": "Validation failed",
  "errors": {
    "body":    "Either body, htmlBody, or templateId is required",
    "channel": "Channel is required"
  }
}

429: Rate limit exceeded

{ "status": 429, "message": "Rate limit exceeded. Retry after 43s", "timestamp": "..." }

Get All Message Logs

GET http://localhost:8080/api/messages/logs
X-API-Key: your-api-key

Response

[
  {
    "id":           "65f1a2b3c4d5e6f7a8b9c0d1",
    "userId":       3,
    "channel":      "SMS",
    "recipient":    "233592548849",
    "body":         "Hello from Kalert API!",
    "status":       "SENT",
    "sentAt":       "2026-03-27T11:25:38Z",
    "errorMessage": null
  }
]

Get Logs by Channel

GET http://localhost:8080/api/messages/logs/channel/EMAIL
X-API-Key: your-api-key

Valid values: SMS, EMAIL, WHATSAPP (case-insensitive)

Get Logs by Status

GET http://localhost:8080/api/messages/logs/status/FAILED
X-API-Key: your-api-key

Valid values: SENT, FAILED

Get Logs by User

Users can only retrieve their own logs. A mismatched userId returns 403 Forbidden.
GET http://localhost:8080/api/messages/logs/user/3
X-API-Key: your-api-key