Kalert Messaging API
Async via Kafka · Multi-channel · Single endpoint
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.
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| Status | Behaviour |
|---|---|
| active / enabled | Accepted |
| disabled | Rejected with 401 |
| Missing key | Rejected with 401 Invalid API key |
Sender ID Endpoints (SMS)
Manage SMS Sender IDs. Each sender starts with pending status and must be approved before use.
Request a Sender ID
/api/senders/requestPOST http://localhost:8080/api/senders/request
X-API-Key: your-api-key
Content-Type: application/json
{
"senderName": "MySender",
"purpose": "Transaction alerts"
}Check Sender Status
/api/senders/{senderName}/statusGET http://localhost:8080/api/senders/MySender/status
X-API-Key: your-api-keyList Sender Records
/api/sendersGET http://localhost:8080/api/senders
X-API-Key: your-api-keyErrors & Response Codes
All error responses follow a consistent JSON structure:
{
"status": 401,
"message": "Invalid API key",
"timestamp": "2026-03-27T10:00:00Z"
}| Code | Meaning | Common cause |
|---|---|---|
| 202 | Accepted | Message queued successfully |
| 200 | OK | Log query returned successfully |
| 400 | Bad request | Missing or invalid request body fields |
| 401 | Unauthorized | Invalid, missing, or disabled API key; unapproved sender |
| 402 | Payment required | Insufficient credits |
| 429 | Too many requests | Rate limit of 100 req/min exceeded |
| 500 | Server error | Unexpected internal error |
Rate Limiting
Each API key is limited to 100 requests per minute. The bucket refills fully every 60 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.
| Channel | Rate | Notes |
|---|---|---|
| SMS | 1 credit per 160-char segment | Long messages split into segments, billed per segment |
| 2 credits per message | Each send costs 2 credits regardless of length | |
| 1 credit per email | Single email to one recipient is always 1 credit |
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.
/api/messages/sendCommon request fields
| Field | Type | Description |
|---|---|---|
| channel | string | SMS, EMAIL, or WHATSAPP |
| recipients | array | List of phone numbers or email addresses |
| senderName | string | Approved SMS Sender ID (SMS only) |
| senderEmail | string | Verified sender email address (EMAIL only) |
| senderDisplayName | string | Friendly name shown next to the From address (EMAIL only) |
| body | string | Plain-text content (SMS, WhatsApp, and email fallback) |
| subject | string | Email subject line (EMAIL only) |
| htmlBody | string | HTML email content (EMAIL only) |
| templateId | number | Optional. Reference a saved email template instead of sending subject/htmlBody/body inline. Convenience only — inline HTML works fine (EMAIL only) |
| templateVariables | object | Global Handlebars variables applied to all recipients (EMAIL only) |
| recipientVariables | object | Per-recipient variable overrides keyed by address (EMAIL only) |
| applyBranding | boolean | Optional. 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 / scheduleDate | bool / string | Set 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" }
}
}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"
}recursion value on the same endpoint (daily, weekly, monthly, or one of the other recognised cadences).SMS
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"
}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
/api/email/sendersSend 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"
}
]
}List sender identities
/api/email/sendersGET http://localhost:8080/api/email/senders
X-API-Key: your-api-keyTrigger re-verification
/api/email/senders/{id}/verifyPOST http://localhost:8080/api/email/senders/12/verify
X-API-Key: your-api-keyRemove a sender identity
/api/email/senders/{id}DELETE http://localhost:8080/api/email/senders/12
X-API-Key: your-api-keyAdvanced send
Body vs. HTML body
Every send needs message content. You choose how to provide it:
htmlBodyalone is fully supported — you do NOT need to also sendbody. Recipients see the rendered HTML in every modern mail client.bodyalone 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 readbodyas 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" }
}
}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
}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
/api/email/templatesGET http://localhost:8080/api/email/templates
X-API-Key: your-api-keyCreate a template
/api/email/templatesPOST 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
/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
/api/email/templates/{id}DELETE http://localhost:8080/api/email/templates/5
X-API-Key: your-api-keySuppression 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
/api/email/suppressionsGET http://localhost:8080/api/email/suppressions
X-API-Key: your-api-keyResponse
[
{
"email": "bounced@example.com",
"reason": "BOUNCE",
"createdAt": "2026-08-10T12:00:00Z"
}
]Add an address to suppression
/api/email/suppressionsPOST 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
/api/email/suppressions/{email}DELETE http://localhost:8080/api/email/suppressions/user@example.com
X-API-Key: your-api-keyWebhook 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
/api/webhooksPOST 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
/api/webhooksGET http://localhost:8080/api/webhooks
X-API-Key: your-api-keyTest a webhook
/api/webhooks/{id}/testPOST http://localhost:8080/api/webhooks/3/test
X-API-Key: your-api-keyEvent 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
| Event | Meaning | Action taken |
|---|---|---|
| DELIVERED | Accepted by recipient server | None |
| OPENED | Recipient opened the email | None |
| CLICKED | Recipient clicked a link | None |
| BOUNCED | Hard bounce (invalid address) | Address added to suppression; credit refunded |
| COMPLAINED | Spam complaint filed | Address added to suppression |
| FAILED | Dispatch error (all retries exhausted) | Credit refunded |
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
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" }
}
}{{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
/api/whatsapp/templatesGET http://localhost:8080/api/whatsapp/templates
X-API-Key: your-api-keyResponse
[
{
"id": 1,
"name": "order_confirmation",
"language": "en",
"status": "APPROVED",
"category": "TRANSACTIONAL"
}
]Sync templates from Meta
/api/whatsapp/templates/syncPOST http://localhost:8080/api/whatsapp/templates/sync
X-API-Key: your-api-keyOpt-outs and inbox
List opt-outs
/api/whatsapp/opt-outsGET http://localhost:8080/api/whatsapp/opt-outs
X-API-Key: your-api-keyAdd an opt-out
/api/whatsapp/opt-outsPOST http://localhost:8080/api/whatsapp/opt-outs
X-API-Key: your-api-key
Content-Type: application/json
{ "phone": "+233244000000" }Remove an opt-out
/api/whatsapp/opt-outs/{phone}DELETE http://localhost:8080/api/whatsapp/opt-outs/%2B233244000000
X-API-Key: your-api-keyInbound messages (inbox)
View messages sent by users to your WhatsApp number and reply to them.
/api/whatsapp/inboxGET http://localhost:8080/api/whatsapp/inbox
X-API-Key: your-api-keyReply to an inbound message
/api/whatsapp/inbox/replyPOST 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:
| Event | Meaning | Action |
|---|---|---|
| DELIVERED | Message delivered to device | None |
| READ | Recipient opened the message | None |
| FAILED | Delivery 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-keyResponse
[
{
"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-keyValid values: SMS, EMAIL, WHATSAPP (case-insensitive)
Get Logs by Status
GET http://localhost:8080/api/messages/logs/status/FAILED
X-API-Key: your-api-keyValid values: SENT, FAILED
Get Logs by User
userId returns 403 Forbidden.GET http://localhost:8080/api/messages/logs/user/3
X-API-Key: your-api-key