TryTumaAPI docs

TryTuma API

Send WhatsApp messages — OTPs, alerts, order updates, and notifications — to your customers from your own number, over a simple REST API.

Overview

The TryTuma 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 URLhttps://trytuma.com
AuthAPI key in the X-Api-Key header
Content typeapplication/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://trytuma.com/v1/messages \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"to": "254712345678", "message": "Hello from TryTuma 👋"}'
# 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
Keep keys server-side. Never ship a 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

FieldTypeNotes
to *stringRecipient in international format (e.g. 254712345678).
messagestringText body. Required unless you send media.
fromstringWhich connected number to send from. Defaults to your session if you have one.
media_urlstringPublic URL of an image/document to attach (see Media).
curl https://trytuma.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 TryTuma sees the same key again it returns the original result instead of sending twice.

As an additional safety net, identical WhatsApp Lite requests without this header are automatically collapsed for 30 seconds. Keep sending your own key: it protects retries for as long as your integration needs, not only the short automatic window.

curl https://trytuma.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

Create groups and manage their recipients in Dashboard → Contacts. Add contacts individually or upload a CSV with phone and optional name columns. Preview up to 1,000 rows before confirming; repeated numbers in the same group are skipped. API additions use the same duplicate protection.

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

Prefer the dashboard? Open Schedules to schedule a WhatsApp text to one number or a saved group. Choose the first run for recurring reminders, edit pending messages, and pause or resume reminders. Times use your workspace timezone. Resuming skips missed occurrences; a send already in progress may finish.

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.

Recurring intervals follow UTC; monthly dates clamp to the last day of shorter months. A processed group job or a reminder's last run is not a delivery confirmation. Check Messages for each recipient's result.

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.

Pro tier: official Cloud API

TryTuma sends over two engines, behind the same API:

Routing is automatic — a request with message goes over Lite; a request with a template (and the OTP & credits endpoints) uses Pro.

Pro is metered in prepaid credits and must be enabled on your workspace. Until it's switched on, the endpoints in this section return 403 cloud_disabled — talk to us to enable it, then top up with M‑Pesa.

Template messages (Pro)

POST /v1/messages — send a pre‑approved template instead of free text.

Send a template object in place of message (never both — that's a 422). Choose a template by name and pass its variables in order. Discover what's available programmatically with GET /v1/templates — it lists every template's name, category, expected variables and credit cost. A wrong variable count returns 422 invalid_template_variables (the template exists); an unknown name returns 422 unknown_template.

nameCategoryvariablesRenders
otpauthentication[code]Verification code + "Copy code" button. Prefer managed OTP, which generates & verifies the code for you.
receiptutility[amount, ref]"Payment received. Amount: … Reference: …"
reminderutility[subject, due]"Reminder: … is due on …"
alertutility[message]"Account notification: …"
order_confirmedutility[order_ref, total]"Your order … is confirmed. Total: …"
delivery_updateutility[order_ref, status, eta]"Update on your order …: it is now …. Estimated arrival: …"
low_datamarketing[plan, remaining]"Heads up: your … data bundle is almost used up, with … remaining. Top up now…" — a purchase prompt, so it bills at the marketing rate (12 credits).
payment_failedutility[amount, service]"We could not process your payment of … for …"
curl https://trytuma.com/v1/messages \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "254712345678",
    "template": { "name": "receipt", "variables": ["KES 1,500", "TXN-48210"] }
  }'

Returns 201 {"id": 278, "status": "sent"} and debits 3 credits (authentication & utility templates) or 12 credits (marketing templates, e.g. low_data). A wrong variable count returns 422 invalid_template_variables; an unknown name 422 unknown_template; too little balance 402 insufficient_credits.

Managed OTP (Pro)

Let TryTuma own the whole one‑time‑password flow: it generates the code, sends it over the approved authentication template, stores only a hash, and verifies it for you. You never handle or store the code.

Send a code

POST /v1/otp/send

curl https://trytuma.com/v1/otp/send \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"to": "254712345678"}'
→ 201 {"id": 278, "status": "sent"}   // debits 3 credits

Verify a code

POST /v1/otp/verify — free, no credit charged

curl https://trytuma.com/v1/otp/verify \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"to": "254712345678", "code": "482913"}'
→ 200 {"status": "verified"}

status is one of: verified, invalid (wrong code), or expired (no active code — it lapsed, was already used, or hit the attempt limit).

Codes expire after 10 minutes, allow 5 attempts, and are single‑use — once verified (or expired) a code can never verify again. Only one active code exists per recipient at a time; sending a new one replaces the previous.
Abuse limits (per recipient number): resending within 60 seconds returns 429 otp_cooldown (with a retry hint); at most 5 codes per rolling hour (429 otp_rate_limited); and at most 10 verification attempts per rolling hour across all codes — every non-successful verify counts (wrong guesses, guesses while locked, guesses against expired codes), so requesting a fresh code never resets the budget. While the budget is spent, /v1/otp/send also refuses (429 otp_locked with retry_after) — you're never charged for a code that could not verify. Note: verify outcomes (verified/invalid/expired/too_many_attempts) always return HTTP 200 with the outcome in status — a lockout is a result, not a transport error.

Credits & usage (Pro)

Pro sends (templates + OTP) are metered in prepaid credits (1 credit = KES 1). Top up with M‑Pesa. Per-send costs: OTP send 3 · utility template 3 · marketing template 12 · SMS 1 per segment (when live) · OTP verify free. GET /v1/templates lists each template's exact cost.

Buy credits (M‑Pesa)

POST /v1/credits/topup

curl https://trytuma.com/v1/credits/topup \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"amount_kes": 500, "phone": "254712345678"}'
→ 201 {"topup_id": 12, "checkout_request_id": "ws_CO_..."}

This triggers an M‑Pesa STK prompt on phone. Credits are added only after the payment confirms (TryTuma verifies it out‑of‑band), so poll /v1/usage a few seconds after the customer approves. Credits granted = amount_kes × the credits‑per‑KES rate.

Check balance & usage

GET /v1/usage

{
  "balance": 99,
  "used_this_period": 3,
  "by_category": { "auth": 2, "utility": 1 }
}

used_this_period is credits spent on Cloud sends since the start of the current calendar month.

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 TryTuma 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, TryTuma generates one and returns it. Every delivery includes an X-Tuma-Signature header so you can verify it came from TryTuma.

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.

SMS (every plan)

TryTuma's second channel: plain SMS to any Kenyan mobile number — no WhatsApp required on the recipient's phone. Available on every plan (including Starter with the 7-day free trial), metered in the same prepaid credits as Pro. Perfect for voucher codes, login OTPs, and payment notices that must reach absolutely everyone.

SMS is launching soon — until the channel is switched on, these requests return 403 sms_disabled. Everything below is live API surface you can integrate against today.

Send an SMS

Add "channel": "sms" to the same POST /v1/messages you already use — omitting it sends over WhatsApp exactly as before. SMS is text-only (no media or templates), Kenya-only for now (2547…/2541…, or the local 07…/01… forms).

curl https://trytuma.com/v1/messages \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "254712345678",
    "message": "Your voucher: WIFI-4G7K2. Valid 24h.",
    "channel": "sms"
  }'
→ 201 {"id": 812, "status": "sent", "segments": 1}

Pricing is per segment: 1 credit per SMS part (KES 1). A message up to 160 GSM characters is one segment; longer texts split at 153 characters per part. Emoji or special characters switch the message to Unicode (70 / 67 characters per part) — the response's segments field always shows what you were billed.

"channel": "sms" also works in bulk sends and single scheduled sends. Delivery updates arrive on the same message.status webhook (sent → delivered, or failed with a reason). Messages go out under the TRYTUMA sender name.

OTP over SMS

Managed OTP works over SMS too — add "channel": "sms" to /v1/otp/send (1 credit instead of WhatsApp's 3); /v1/otp/verify is unchanged and works for codes sent over either channel:

curl https://trytuma.com/v1/otp/send \
  -H "X-Api-Key: tuma_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "to": "254712345678", "channel": "sms" }'
→ 201 {"id": 813, "status": "sent"}   // debits 1 credit
Transactional only. TryTuma SMS is built for transactional traffic — codes, receipts, reminders, service notices. Promotional/marketing SMS in Kenya carries extra obligations (recipient opt-in, opt-out handling, daytime-only sending under CA rules) that remain your responsibility; promotional tooling is on our roadmap.

Billing & fintech recipes

TryTuma sits next to your payment rails — keep collecting money your way (M-Pesa, cards, bank) and call TryTuma 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://trytuma.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."}'
For high-volume OTP, the compliant, ban-resistant path is the official WhatsApp Cloud API tier (on TryTuma's roadmap). Phase-1 sending suits notifications and lighter verification volumes.

Payment confirmation / receipt

Send this from your M-Pesa/Daraja callback once a payment is verified.

curl https://trytuma.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 TryTuma 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://trytuma.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 TryTuma 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://trytuma.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,
    )
TryTuma sends messages — it does not collect payments. Keep your existing gateway for money movement; call TryTuma to keep customers informed around it (OTPs, receipts, reminders, alerts).

Errors

Errors use standard HTTP status codes and a small JSON envelope:

{"error": "invalid_request", "reason": "to is required"}
StatusMeaning
400 / 422Invalid request — a field is missing or malformed.
401Missing or invalid API key.
402Subscription past due — settle billing to resume.
403Workspace suspended.
404Resource not found.
502The gateway rejected the send, or an identical failed retry was suppressed. Read error and reason before retrying.
503The WhatsApp session or gateway is temporarily unavailable. When reason includes “Retry after”, wait that many seconds before retrying.

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.

Ready to try it live? Open the interactive reference, paste your key, and send a real request — or import /openapi.json into Postman.