Luma
Log in
Developers

One SMS API. Nothing else to configure.

Authenticate with an API key. Send a transactional message to one Nigerian number. Check its status, list your sender IDs, check your wallet balance. That's the whole surface — small and predictable, five endpoints, nothing hidden.

curl -X POST https://api.uselumaapp.com/v1/sms/send \
-H "X-API-Key: luma_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"to": "2348012345678",
"message": "Your OTP is 123456",
"senderId": "ADAFAB"
}'

Five endpoints. That's the whole surface.

One header: X-API-Key

Created, rotated, and revoked from the dashboard — never over the API. Authorization: Bearer is rejected.

₦7 per SMS page

Charged per page (segment) on a successful send — a longer message spanning more pages costs proportionally more.

60 requests/minute

One fixed-window limit, shared across every /v1 endpoint combined — not a per-endpoint allowance.

Two sender ID types

Transactional is the only kind usable through this API. Both types are registered from the dashboard, not by API call.

Account-level webhooks

5 events, HMAC-signed, dashboard-configured. Not built for per-message delivery — poll GET /v1/status for that.

Live OpenAPI reference

Auto-generated Swagger docs at /docs, kept in sync with what's actually deployed — the canonical schema.

Base URL

Every request in this reference is made to:

https://api.uselumaapp.com

Each endpoint below is relative to this host, under /v1. For the complete, interactive, always-current schema — every field, every error — see the live Swagger reference at /docs. It's generated directly from the API, so it can't drift from what's actually deployed; this page is the fast overview, not a replacement for it.

Authentication

One header, one key.

Every request needs:

X-API-Key: luma_live_xxxxxxxxxxxxxxxxxxxxxxxx

Keys are 42 characters: the luma_live_ prefix followed by 32 random base64url characters. There's no test or sandbox key — every key is live, and every send it makes is real and billed.

Authorization: Bearer is rejected outright — that header is reserved for internal service tokens. Use X-API-Key.

Keys are created, rotated, and revoked from the dashboard (Developer → API Keys) — there's no endpoint for any of that. The full secret is shown exactly once, at creation or rotation; store it then, because you won't see it again.

Field
Type
Notes
401
Unauthorized
Missing or invalid API key.
403
Forbidden
Key is valid, but the business isn't approved yet, or has been disabled.
Endpoints

The complete API surface

This is the entire list — no bulk send, no contacts API, no sender ID submission through this API.

POST/v1/sms/sendSend one message to one recipient
GET/v1/smsPaginated send history
GET/v1/statusStatus of one send
GET/v1/senderIdsList your sender IDs (read-only)
GET/v1/balanceCurrent wallet balance

Sends a single SMS to a single recipient. There is no bulk or multi-recipient variant of this call.

Field
Type
Notes
to
string, required
Nigerian MSISDN, e.g. 2348012345678.
message
string, required
No length cap enforced by the API — longer messages simply span more pages (see Pricing).
senderId
string, optional
Required only if your business has more than one approved transactional sender ID. If omitted and none is registered, falls back silently to a platform default (LUMA or N-Alert).
curl -X POST https://api.uselumaapp.com/v1/sms/send \
  -H "X-API-Key: luma_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "2348012345678",
    "message": "Your OTP is 123456",
    "senderId": "ADAFAB"
  }'
201 Response
{
  "status": "success",
  "message": "Request successful",
  "data": {
    "success": true,
    "providerReference": "ref-1",
    "sendRequestId": "sr-1"
  }
}
A rejected send can still come back as HTTP 201. Check data.success, not just the status code.
Response format & errors

One envelope, every response.

{ "status": "success" | "failed", "message": string, "data": ... | null }

An error looks like this:

{
  "status": "failed",
  "message": "\"123\" is not a recognizable Nigerian phone number.",
  "data": { "code": "INVALID_MSISDN" }
}
400Validation error — INVALID_MSISDN, INSUFFICIENT_FUNDS, or AMBIGUOUS_SENDER_ID.
401Missing or invalid API key.
403Key is valid, but the business isn't approved yet, or has been disabled.
404NO_TRANSACTIONAL_SENDER_ID, or an unknown send id.
429Rate limit exceeded — 60 requests/minute, shared across all /v1 endpoints.
503Upstream provider temporarily unreachable.
500Unexpected server error.
Rate limits

60 requests per minute per API key, shared across every /v1 endpoint combined — not a per-endpoint allowance. It's a fixed window, and the response carries no Retry-After or X-RateLimit-* header. On a 429, back off — there's no header telling you exactly how long.

Pricing

₦7 per SMS page, charged only when a send is actually accepted for delivery — a failed send (invalid number, insufficient funds, provider rejection) isn't billed. A page is one SMS segment; a message that spans more pages costs proportionally more — a 2-page message costs ₦14, not ₦7.

Delivery status

Read this before you build on it.

GET /v1/status returns two genuinely different things. status reflects whether the message was accepted for sending — that updates quickly and is reliable. delivered and undelivered reflect carrier-level delivery confirmation, and whether that's available depends on the route a given send was carried on.

Some routes report delivery confirmation back within minutes, polled for up to 24 hours after send. On others, delivery confirmation isn't available yet, and a message can be fully sent and billed while showing delivered: 0, undelivered: 0 forever — that's not a bug in your integration, it's the current state of delivery confirmation on this API. Build success criteria around status, not around delivered ever reaching recipientCount.
Webhooks

Configured from the dashboard (Developer → Webhooks) — there's no API for creating or managing them. Five events, and this is the complete list:

campaign.completedwallet.fundedkyc.status_changedsender_id.approvedsender_id.rejected
There is no per-message delivery webhook. To know when an individual send was delivered, poll GET /v1/status.

Payload shape:

{ "event": "wallet.funded", "occurredAt": "2026-09-01T09:00:00Z", "data": { ... } }

Every request is signed — HMAC-SHA256 over the raw request body, sent as:

X-Luma-Signature: sha256=<hex>

Hash the raw bytes you received — not a re-serialized copy of the parsed JSON. Re-encoding the payload (different key order, different whitespace) produces a different hash, and the signature will fail to verify even though the payload is "the same" data.

Retried up to 5 times on failure, with backoff of 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours. After 20 consecutive failures, the webhook is disabled automatically. The endpoint URL must be HTTPS.

Sender IDs

Two traffic types: transactional and promotional. Only transactional sender IDs can be used through this API — GET /v1/senderIds lists both if you have them, but promotional traffic isn't sent through this API. Registering a new sender ID happens in the dashboard, not here; approval touches all four Nigerian mobile operators (MTN, Airtel, Glo, 9mobile) and is manual on the operators' side, typically taking 4–6 weeks. If you don't specify a senderId on a send and don't have one approved, the message goes out under a platform default (LUMA or N-Alert).

Start building in minutes.

One API key. No sandbox required.