Tuma API
Send WhatsApp messages — OTPs, alerts, order updates, and notifications — to your customers from your own number, over a simple REST API.
Overview
The Tuma API is HTTPS + JSON. You authenticate with an API key, connect a WhatsApp number once, then send messages with a single request. Delivery status streams back to your app through webhooks.
| Base URL | https://tuma.wencreatives.com |
|---|---|
| Auth | API key in the X-Api-Key header |
| Content type | application/json |
| Interactive | /reference (try calls in the browser) · /openapi.json (import into Postman/Insomnia) |
Quickstart
Three steps to your first message.
1. Get an API key
Create an account, then open API keys in the dashboard and create a key. It starts with tuma_live_ and is shown once — store it securely.
2. Connect a WhatsApp number
In the dashboard, open Sessions → Connect a number and scan the QR with the phone whose number you want to send from. (You can also create the session via the API.) Use a dedicated number, not your personal line.
3. Send your first message
# replace the key and recipient curl https://tuma.wencreatives.com/v1/messages \ -H "X-Api-Key: tuma_live_your_key" \ -H "Content-Type: application/json" \ -d '{"to": "254712345678", "message": "Hello from Tuma 👋"}'
# 201 Created
{"id": 45, "status": "sent"}Authentication
Every request to /v1 must include your API key in the X-Api-Key header. Keys are tenant-scoped — a key only ever sees its own workspace's data. Create and revoke keys anytime from the dashboard.
X-Api-Key: tuma_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
tuma_live_ key in a browser, mobile app, or public repo. Rotate immediately if one leaks (revoke in the dashboard).Phone number format
Recipients (to / numbers) are international numbers in digits only — no +, spaces, or leading zero. A Kenyan number 0712 345 678 becomes 254712345678. The recipient must be on WhatsApp; you cannot send to the same number that is linked to the session.
Send a message
POST /v1/messages
| Field | Type | Notes |
|---|---|---|
to * | string | Recipient in international format (e.g. 254712345678). |
message | string | Text body. Required unless you send media. |
from | string | Which connected number to send from. Defaults to your session if you have one. |
media_url | string | Public URL of an image/document to attach (see Media). |
curl https://tuma.wencreatives.com/v1/messages \
-H "X-Api-Key: tuma_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "254712345678",
"message": "Your verification code is 481920. It expires in 10 minutes."
}'Returns 201 with the stored message id and its current status:
{"id": 45, "status": "sent"}Track the final delivery outcome with GET /v1/messages/{id} or, better, a webhook.
Media & documents
Attach an image or document by URL or inline base64. Add a caption with message.
{
"to": "254712345678",
"message": "Your receipt",
"media_url": "https://example.com/receipt.pdf",
"filename": "receipt.pdf",
"mimetype": "application/pdf"
}Or inline: send media_base64 with filename and mimetype instead of media_url.
Idempotency
Retries are safe. Send an Idempotency-Key header with a unique value per logical message; if Tuma sees the same key again it returns the original result instead of sending twice.
curl https://tuma.wencreatives.com/v1/messages \
-H "X-Api-Key: tuma_live_your_key" \
-H "Idempotency-Key: order-10482-otp" \
-H "Content-Type: application/json" \
-d '{"to":"254712345678","message":"Code: 481920"}'Bulk send
POST /v1/messages/bulk — the same text to many recipients. Sends run in the background with jitter to protect the number.
{
"numbers": ["254712345678", "254798765432"],
"message": "Flash sale today only — 20% off. Reply STOP to opt out."
}Groups
Save recipient lists once and send to them by id.
POST /v1/groups — create · POST /v1/groups/{id}/contacts — add numbers · POST /v1/groups/{id}/send — broadcast
# create a group {"name": "VIP customers"} # add contacts {"contacts": ["254712345678", "254798765432"]} # send to everyone in the group {"message": "Early access opens at 9am."}
Scheduled sends & reminders
POST /v1/scheduled queues a one-off send for a future time. type is single, group, or bulk; give scheduled_at as an ISO-8601 timestamp.
{"type":"single","to":"254712345678","message":"Your appointment is tomorrow at 10am.","scheduled_at":"2026-09-20T07:00:00Z"}POST /v1/reminders creates a recurring send. unit is day, week, or month; every is the interval.
{"name":"Weekly rent reminder","every":1,"unit":"week","to":"254712345678","message":"Rent is due Friday."}List, update (PATCH), and cancel (DELETE) both from the same paths.
Inbox (replies)
Incoming replies from your customers are captured. GET /v1/inbox lists them; DELETE /v1/inbox/{id} removes one. For real-time replies, use the message.received webhook.
Connect a number via the API
Most people connect in the dashboard, but you can script it.
POST /v1/sessions — create a session
{"display_name": "Sokoni Store line"}
→ 201 {"instance_name": "org3_ab12cd34", "status": "starting"}GET /v1/sessions/{name}/qr returns the QR to scan; GET /v1/sessions/{name}/status reports {"status": "..."} — poll until it reads connected.
Webhooks
Register one HTTPS endpoint and Tuma will POST events to it — delivery updates and inbound replies — so you never have to poll.
POST /v1/webhooks — set your endpoint
{"url": "https://your-app.com/tuma/webhook", "enabled": true}If you don't supply a secret, Tuma generates one and returns it. Every delivery includes an X-Tuma-Signature header so you can verify it came from Tuma.
Events
// message.status — a message you sent changed state { "event": "message.status", "message_id": 45, "wa_message_id": "3EB0A1DBF3B1184CD6951", "to_number": "254712345678", "status": "delivered", "timestamp": "2026-09-18T09:41:00Z" } // message.received — a customer replied { "event": "message.received", "from_number": "254712345678", "body": "Yes, confirm my order", "wa_message_id": "3EB0...", "session": "org3_ab12cd34", "timestamp": "2026-09-18T09:42:00Z" }
Verify the signature
The header is X-Tuma-Signature: sha256=<hex> — an HMAC-SHA256 of the exact raw request body, keyed with your webhook secret. Always compute it over the raw bytes you received.
# Python (Flask)
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")// Node (Express, express.raw() body)
const crypto = require("crypto");
function verify(secret, rawBody, header) {
const digest = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return header && crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(header));
}Manage the endpoint with GET /v1/webhooks and DELETE /v1/webhooks.
Billing & fintech recipes
Tuma sits next to your payment rails — keep collecting money your way (M-Pesa, cards, bank) and call Tuma on each billing event to message the customer. Add an Idempotency-Key to anything you might retry, and register a webhook to record delivery.
Verification code (OTP)
Generate and store the code in your own app (hashed, short expiry), send it, and verify it on your side. The idempotency key stops a network retry from sending two codes.
curl https://tuma.wencreatives.com/v1/messages \
-H "X-Api-Key: tuma_live_your_key" \
-H "Idempotency-Key: login-8842-1c9f2a" \
-H "Content-Type: application/json" \
-d '{"to":"254712345678","message":"Your code is 481920. It expires in 10 minutes. Do not share it."}'Payment confirmation / receipt
Send this from your M-Pesa/Daraja callback once a payment is verified.
curl https://tuma.wencreatives.com/v1/messages \
-H "X-Api-Key: tuma_live_your_key" \
-H "Idempotency-Key: receipt-10482" \
-H "Content-Type: application/json" \
-d '{"to":"254712345678","message":"Payment received: KES 2,500 for invoice #10482. Receipt SKL3X21AB. Thank you!"}'Invoice due / renewal reminder
Send it now from your scheduler, or let Tuma hold it until the due date — or repeat it.
# a one-off reminder queued for the due date POST /v1/scheduled {"type":"single","to":"254712345678","message":"Invoice #10482 (KES 2,500) is due on 25 Sep.","scheduled_at":"2026-09-25T06:00:00Z"} # or a recurring monthly reminder POST /v1/reminders {"name":"Rent due","every":1,"unit":"month","to":"254712345678","message":"Your rent is due this week."}
Failed payment / retry nudge
curl https://tuma.wencreatives.com/v1/messages \
-H "X-Api-Key: tuma_live_your_key" \
-H "Content-Type: application/json" \
-d '{"to":"254712345678","message":"We couldn'\''t process your renewal. Pay here to stay active: https://pay.example.com/r/abc123"}'Confirm it was delivered
Keep the id returned by each send, then match it to the message.status webhook's message_id to mark the notification delivered or failed in your system. Use message.received to capture replies like PAID or STOP.
Putting it together
A billing backend just calls Tuma when an event fires:
# Python — notify the customer when your billing system confirms a payment
import httpx
def notify_payment(phone, amount, receipt, invoice_id):
httpx.post(
"https://tuma.wencreatives.com/v1/messages",
headers={
"X-Api-Key": "tuma_live_your_key",
"Idempotency-Key": f"receipt-{invoice_id}",
},
json={
"to": phone,
"message": f"Payment received: KES {amount:,} for invoice #{invoice_id}. Receipt {receipt}.",
},
timeout=15,
)Errors
Errors use standard HTTP status codes and a small JSON envelope:
{"error": "invalid_request", "reason": "to is required"}| Status | Meaning |
|---|---|
400 / 422 | Invalid request — a field is missing or malformed. |
401 | Missing or invalid API key. |
402 | Subscription past due — settle billing to resume. |
403 | Workspace suspended. |
404 | Resource not found. |
502 / 503 | The WhatsApp gateway rejected the send or is unreachable — safe to retry. |
Rate limits
The messaging API isn't hard rate-limited today, but WhatsApp itself throttles bursts and can restrict a number that sends too aggressively. Send at a human pace, use bulk (which paces automatically), and always give recipients a way to opt out.