Skip to main content

SMS API

SMS API for Tanzania: send with one HTTPS request

A versioned REST API with scoped keys, idempotency, signed webhooks and delivery status for every message. Create a key in your dashboard and send your first SMS in minutes.

Base URL
https://api.lundosms.co.tz/v1
Authentication
Authorization: Bearer YOUR_API_KEY
Format
JSON over HTTPS
Per request
Up to 1,000 recipients

Quick start

Four steps from nothing to a delivered message.

  1. 1

    Create an API key

    In your dashboard, open the Developer section and create a key. Start with a test key (lsk_test_…); live keys start with lsk_live_…. Copy the key when it is shown — it appears only once.

  2. 2

    Have a sender ID approved

    Messages are sent with an approved sender ID. Request yours from the dashboard; the API returns sender_id_not_approved until it is approved.

  3. 3

    Send a message

    POST to /v1/messages with your sender ID, the recipients and the text. Numbers can be written as 0712 345 678, 255712345678 or +255712345678 — they come back in international format.

    POST /v1/messages

    curl -X POST 'https://api.lundosms.co.tz/v1/messages' \
      -H 'Authorization: Bearer YOUR_API_KEY' \
      -H 'Idempotency-Key: order-1001' \
      -H 'Content-Type: application/json' \
      -d '{
        "sender_id": "YOURBRAND",
        "recipients": ["255712345678"],
        "message": "Your order has been confirmed.",
        "client_reference": "ORDER-1001",
        "type": "transactional"
      }'
  4. 4

    Read the response

    A 202 means the messages are accepted and their credits reserved. Each message has its own id: fetch its status from GET /v1/messages/<id>, or let a webhook tell you when it changes.

What you get back

The 202 response lists every accepted message with its id and status. Recipients that cannot be accepted — an invalid number, an opted-out number, a duplicate — are returned in rejected_recipients with a reason, while the valid ones go ahead. If none is valid, the request fails with 422 no_valid_recipients; if there are not enough credits, with 402 insufficient_credits and nothing is sent.

202 Accepted
{
  "success": true,
  "code": "messages_accepted",
  "message": "1 message accepted.",
  "data": {
    "batch_id": "0192c0d1-7c3e-7a52-9f31-2b6c8a1d4e90",
    "status": "queued",
    "accepted": 1,
    "rejected": 0,
    "encoding": "gsm7",
    "segments_per_message": 1,
    "credits_reserved": 1,
    "messages": [
      {
        "id": "0192c0d1-7c3e-7a52-9f31-2b6c8a1d4e91",
        "to": "+255712345678",
        "status": "queued",
        "client_reference": "ORDER-1001"
      }
    ],
    "rejected_recipients": []
  },
  "request_id": "0192c0d1-7c3e-7a52-9f31-2b6c8a1d4e92"
}

Endpoints

The customer REST API
EndpointWhat it doesScope
POST /v1/messagesSend one or many messagesmessages:send
POST /v1/messages/estimateEncoding, segments and credits for a textmessages:send
GET /v1/messages/<id>Status, segments and timestamps of one messagemessages:read
GET /v1/messagesList and filter message history (cursor paging)messages:read
GET /v1/balanceAvailable and reserved creditsbalance:read
GET /v1/sender-idsYour approved sender IDssender_ids:read
GET · POST · DELETE /v1/webhooksCreate, list and delete webhookswebhooks:manage
GET /v1/openapi.yamlThe OpenAPI 3.1 contractpublic

Message statuses

"Submitted" means the operator network accepted the message. It is never reported as delivered until the handset confirms.

Message statuses
StatusMeaning
scheduledWaiting for its scheduled time.
queuedAccepted; credits reserved; waiting to be sent.
submittingBeing handed to the operator network.
submittedAccepted by the operator network — not yet delivered.
deliveredThe handset confirmed receipt.
undeliverableThe network could not deliver it (for example an invalid or unreachable number).
expiredThe validity period ended before delivery.
rejectedRejected by the network or before sending.
cancelledCancelled before it was sent; credits released.
unknownNo final receipt arrived in the waiting period.

Idempotency keys

Send an Idempotency-Key header (up to 100 characters) with every send. A retry with the same key and body returns the original response, flagged Idempotent-Replayed: true; the same key with a different body returns 409 idempotency_key_reused. Keys are remembered for 24 hours, so a network timeout can never send an SMS twice.

Errors and limits

  • Every error has the same JSON shape with a stable code — for example insufficient_credits (402), validation_failed (422) or rate_limited (429).
  • Every response carries X-Request-Id and X-RateLimit-* headers; a 429 adds Retry-After.
  • The default limit is 20 requests per second per key (burst 40), and message throughput is also capped per organisation.
  • Unknown JSON fields are rejected, and request bodies are limited to 1 MiB.

Webhooks and signature verification

LundoSMS sends a signed POST to your server on every message status change. Verify the signature before you trust the body.

Headers on every delivery

Headers on every delivery
FieldMeaning
X-LundoSMS-EventThe event type, for example message.status.
X-LundoSMS-DeliveryA UUID that stays the same across retries — use it to ignore duplicates.
X-LundoSMS-Signaturet=<unix time>,v1=<hex HMAC-SHA256>

How to verify a delivery

  1. Take the raw request body, before any JSON parsing.
  2. Read t and v1 from the X-LundoSMS-Signature header.
  3. Compute HMAC-SHA256 of <t>.<raw body> with your webhook secret and compare it with v1 using a constant-time comparison.
  4. Reject the request if t is more than five minutes old.

X-LundoSMS-Signature

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyLundoSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.t || !parts.v1 || age > 300) return false; // older than 5 minutes

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

A non-2xx response or a timeout over 10 seconds is retried after 10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h and 6 h — eight attempts — and then the delivery is marked dead and shown in your dashboard with a retry button.

Example event

POST https://your-server.example/webhooks/lundosms
{
  "id": "evt_0192c0e5a1b2",
  "type": "message.status",
  "created_at": "2026-10-17T09:31:04Z",
  "data": {
    "message_id": "0192c0d1-7c3e-7a52-9f31-2b6c8a1d4e91",
    "to": "+255712345678",
    "status": "delivered",
    "segments": 1,
    "client_reference": "ORDER-1001",
    "error_code": null,
    "submitted_at": "2026-10-17T09:30:01Z",
    "delivered_at": "2026-10-17T09:31:03Z"
  }
}

Test before you go live

Test keys (lsk_test_…) run the same validation and return real references, with simulated delivery receipts. No SMS leaves the platform and no credits are used. Switch to a live key when you are ready.

More inside your dashboard

The Developer section of your dashboard shows your own base URL, approved sender IDs and keys, the OpenAPI reference and ready-to-copy samples in cURL, Go, PHP, Node.js and Python.

Open the developer docs

API questions

Can I send from my own software?

Yes. The REST API accepts a single HTTPS request with an API key, supports idempotency keys so a retry never sends twice, and reports status changes by signed webhook. Test keys let you try everything without sending real messages.

Can I try it before buying credits?

Yes. Create an account, then use a test API key: it goes through the same validation and returns real references, but nothing is sent to a phone and no credits are used.

How do I know a message was delivered?

Every message has its own status: queued, sent to network, delivered, or failed. "Sent to network" means the operator accepted the message; "delivered" means the handset confirmed it. You can see statuses in the dashboard, export them, or receive them by webhook or SMPP. A phone that is switched off or out of coverage may receive a message late or never — no provider can guarantee delivery when the operator does not confirm it.

SMPP integration →

Ready to reach every phone in Tanzania?

Create your account, register your sender name and send your first message today. Buy the SMS credits you need — they never expire.

Prepaid credits only. Prices in TZS, VAT included.