Current

API reference · v2

API documentation

The FasterMessage REST contract: send SMS, WhatsApp messages, emails and voice calls, follow every delivery in real time and steer your balance from a single interface.

Quick start

The FasterMessage API sends messages over four channels — SMS, WhatsApp, email and voice — from a single REST contract. Three steps stand between a new account and a first delivered message.

1. Collect your credentials

In the back office, open Integration & API. There you find your account identifier, and there you generate two keys: a test key and a live key. A key is shown only once — it is stored hashed and cannot be read back.

2. Declare a sender

SMS requires an approved Sender ID: there is no per-account default sender, and a send without from is refused before it is even priced. Declaration happens in the back office, Sender IDs tab.

3. Send

First send
curl -X POST https://api-next.fastermessage.com/v2/sms/send \
  -u "$FM_API_USER:$FM_API_SECRET" \
  -H "X-Api-Key: $FM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+22960000000",
  "from": "FASTERMSG",
  "content": "Your code is 480912. Valid for 5 min."
}'
Response — 202 Accepted
{
  "id": "msg_2f1c9d4e8a6b",
  "channel": "sms",
  "status": "queued",
  "to": "22960000000",
  "from": "FASTERMSG",
  "units": 1,
  "price": null,
  "currency": "XOF",
  "refund": null,
  "client_reference": null,
  "fallback_of": null,
  "error": null,
  "created_at": "2026-09-07T10:00:00.000Z",
  "scheduled_at": null,
  "sent_at": null,
  "delivered_at": null
}

Authentication

Every call carries two credentials, both mandatory: a Basic authentication and an API key. This is not redundancy — the two do not designate the same thing.

CredentialSent throughDesignates
BasicAuthorizationThe account to charge: it is the one paying for the send.
API keyX-Api-KeyThe account making the request — itself, or one of its sub-accounts.

The match between the two is checked on every call: a key belonging neither to the authenticated account nor to one of its sub-accounts is refused. That pairing is what lets a reseller send on behalf of its customers without handing them its password.

Headers
Authorization: Basic bW9uLWNvbXB0ZTpzZWNyZXQ=
X-Api-Key: fm_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Keep both server-side. A key exposed in a web or mobile client is a published key: revoke it from the back office and generate a new one.

Environments

EnvironmentKey prefixBehaviour
testfm_test_…Calls go through exactly the same checks as in production and return the same error codes, but nothing is handed to the carrier, nothing is charged, and no callback URL is called.
productionfm_live_…Real sends, charged to the balance, handed to the carrier, genuine DLRs.

What changes in test

  • No carrier delivery: the recipient receives nothing, however valid their number.
  • No charge — and a balance is not even required: you can write your whole integration before buying anything.
  • Your brand’s Sender IDs can be borrowed even without an approval of their own — which is never the case in production.
  • An immediate send is marked delivered at once; a scheduled send stays pending until its date. No webhook is called: reaching your callback URL would already be a delivery.
  • The two environments are sealed off from each other: a test key reads back only its own simulated sends (GET /v2/messages), never your real traffic — and the other way round. Simulations are kept for 30 days, then erased.
  • Code verification (OTP): the code is always a run of zeros (000000, or 0000 depending on the length requested). Since no SMS goes out, this is what lets you exercise POST /v2/otp/verify end to end.

Try the API

Compose a call with your test credentials and run it from this page. The equivalent snippet is regenerated live in all four languages.

Test environment only — never use your production credentials on this page. The values you type stay in your browser: they are neither stored nor sent to a third party.

Test credentials

The account to charge — the “Basic” half of the authentication.

The second half of the “Basic”. Never displayed, never copied into the samples.

X-Api-Key header. The fm_test_ prefix guarantees no real send is triggered.

Message

11 characters at most. Mandatory — there is no default sender.

E.164 format: country code then number, 8 to 15 digits.

37 characters1 billed segmentGSM-7

curl -X POST https://api-next.fastermessage.com/v2/sms/send \
  -u "$FM_API_USER:$FM_API_SECRET" \
  -H "X-Api-Key: $FM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "",
  "from": "FASTERMSG",
  "content": "Your code is 480912. Valid for 5 min."
}'

Fill in the required fields to run the call.

Single send

Méthode HTTP POSThttps://api-next.fastermessage.com/v2/sms/send

Sends an SMS to one recipient. Returns 202 Accepted and the message resource: delivery itself is asynchronous and is followed by webhook or through GET /v2/messages/{id}.

Request body

FieldTypeDescription
torequiredstringRecipient in E.164 format (8 to 15 digits, + optional).
fromrequiredstringSender ID approved on the account. No default sender is applied; an account in test mode may use its brand’s Sender ID.
contentrequiredstringMessage text. Beyond 160 GSM characters (70 in UCS-2), it is split into segments, each billed individually.
scheduled_atoptionalstringSend date in ISO 8601 UTC. An unreadable date is refused, never ignored.
callback_urloptionalstringCallback URL specific to this message — takes precedence over the account URL.
client_referenceoptionalstringFree-form reference, echoed as-is in the response and in webhooks.
fallbackoptionalarrayOrdered cascade of fallback channels, each step tried only if the previous one did not succeed.

Optional headers

FieldTypeDescription
Idempotency-KeyoptionalstringA unique key of your choosing. A replay with the same key returns the memorised response instead of resending the message. Kept for 24 h.
Request
curl -X POST https://api-next.fastermessage.com/v2/sms/send \
  -u "$FM_API_USER:$FM_API_SECRET" \
  -H "X-Api-Key: $FM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+22960000000",
  "from": "FASTERMSG",
  "content": "Your order cmd-42 is ready.",
  "client_reference": "cmd-42",
  "callback_url": "https://example.test/webhooks/fastermessage"
}'
Success — 202 Accepted
{
  "id": "msg_2f1c9d4e8a6b",
  "channel": "sms",
  "status": "queued",
  "to": "22960000000",
  "from": "FASTERMSG",
  "units": 1,
  "price": null,
  "currency": "XOF",
  "refund": null,
  "client_reference": "cmd-42",
  "created_at": "2026-09-07T10:00:00.000Z"
}
Error — 402 Payment Required
{
  "error": {
    "type": "business_error",
    "code": "insufficient_balance",
    "message": "Insufficient balance to process the request.",
    "request_id": "2f1c9d4e-8a6b-4c1e-9f2a-7d3b5e0c1a84"
  }
}

Bulk sending

Méthode HTTP POSThttps://api-next.fastermessage.com/v2/sms/batch

Up to 500 SMS per call, in a single request — and a single unit as far as rate limits are concerned. Values set at batch level (from, content, scheduled_at, callback_url) are inherited by every line that does not carry its own: the same entry point serves broadcasting (one text, N numbers) and grouped transactional sending (one text per line).

Request body

FieldTypeDescription
messagesrequiredarrayLines of the batch, 500 at most. Each line accepts the fields of a single send.
fromoptionalstringBatch sender, inherited by lines that declare none. Mandatory — on the batch or on every line.
contentoptionalstringBatch text, inherited the same way.
scheduled_atoptionalstringCommon send date, in ISO 8601 UTC.
callback_urloptionalstringCallback URL shared by every line.
Request
curl -X POST https://api-next.fastermessage.com/v2/sms/batch \
  -u "$FM_API_USER:$FM_API_SECRET" \
  -H "X-Api-Key: $FM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "FASTERMSG",
  "content": "Your delivery arrives tomorrow between 9 am and 12 pm.",
  "messages": [
    {
      "to": "+22960000000",
      "client_reference": "cmd-42"
    },
    {
      "to": "+22961000001",
      "client_reference": "cmd-43"
    },
    {
      "to": "+22962000002",
      "content": "Your delivery arrives today.",
      "client_reference": "cmd-44"
    }
  ]
}'
Success — 200 OK (report)
{
  "total": 3,
  "accepted": 2,
  "rejected": 1,
  "data": [
    { "index": 0, "id": "msg_2f1c9d4e8a6b", "status": "queued", "to": "22960000000" },
    { "index": 1, "id": "msg_7d3b5e0c1a84", "status": "queued", "to": "22961000001" }
  ],
  "errors": [
    {
      "index": 2,
      "code": "invalid_number",
      "message": "The recipient number is not valid.",
      "param": "to",
      "to": "2296"
    }
  ]
}

Message status

Méthode HTTP GEThttps://api-next.fastermessage.com/v2/messages/{id}

Returns the message resource, in the exact shape that sending returns: one deserialiser is enough on both sides.

Possible statuses

StatusTerminalMeaning
schedulednoScheduled, waiting for its send date.
queuednoAccepted and queued, not yet handed to the carrier.
sentnoHanded to the carrier, delivery receipt not received.
deliveredyesDelivered to the recipient, confirmed by the carrier.
undeliveredyesNot delivered — number switched off, out of service, mailbox full.
failedyesFailure before delivery: carrier rejection, content refused, gateway error.
Request
curl https://api-next.fastermessage.com/v2/messages/msg_2f1c9d4e8a6b \
  -u "$FM_API_USER:$FM_API_SECRET" \
  -H "X-Api-Key: $FM_API_KEY"
Success — 200 OK
{
  "id": "msg_2f1c9d4e8a6b",
  "channel": "sms",
  "status": "delivered",
  "to": "22960000000",
  "from": "FASTERMSG",
  "units": 1,
  "price": 12,
  "currency": "XOF",
  "refund": null,
  "client_reference": "cmd-42",
  "fallback_of": null,
  "error": null,
  "created_at": "2026-09-07T10:00:00.000Z",
  "scheduled_at": null,
  "sent_at": "2026-09-07T10:00:01.420Z",
  "delivered_at": "2026-09-07T10:00:06.900Z"
}

List messages

Méthode HTTP GEThttps://api-next.fastermessage.com/v2/messages

Query parameters

FieldTypeDescription
channeloptionalstringFilter on one channel: sms, whatsapp, email, voice.
statusoptionalstringFilter on one of the six statuses.
limitoptionalintegerPage size.
cursoroptionalstringPagination cursor — pass back the next_cursor value of the previous page.
Success — 200 OK
{
  "data": [ { "id": "msg_2f1c9d4e8a6b", "status": "delivered" } ],
  "next_cursor": "eyJvIjoxfQ",
  "has_more": true
}

Balance

Méthode HTTP GEThttps://api-next.fastermessage.com/v2/balance

Returns the available balance, channel by channel and in the account currency. Every channel is returned, including at zero: a missing channel would be indistinguishable from an empty balance.

Request
curl https://api-next.fastermessage.com/v2/balance \
  -u "$FM_API_USER:$FM_API_SECRET" \
  -H "X-Api-Key: $FM_API_KEY"
Success — 200 OK
{
  "balances": [
    { "channel": "sms", "amount": 289009, "currency": "XOF" },
    { "channel": "whatsapp", "amount": 12000, "currency": "XOF" },
    { "channel": "email", "amount": 0, "currency": "XOF" },
    { "channel": "voice", "amount": 4500, "currency": "XOF" }
  ]
}

Credit movements

Méthode HTTP GEThttps://api-next.fastermessage.com/v2/credits

Movement log: credit (purchase), debit (usage) and refund, kept distinct from a credit — a system refund is not a purchase.

Senders (Sender ID)

What the API does impose, on every send: from is mandatory on the SMS channel and must match a Sender ID approved on the account. There is no per-account default sender.

SituationBehaviour
from missingRefused with 400 missing_parameter, before any balance check.
from unknown or not approvedRefused with 403. Nothing is charged.
Account in test modeThe brand’s default Sender ID can be borrowed, with no approval of its own.

How to declare one is set out in the back-office documentation.

The rest of the surface

The endpoints below follow the same conventions for authentication, errors and pagination.

MethodPathPurpose
POST/v2/messagesUnified sending — the channel is a body parameter.
POST/v2/whatsapp/sendWhatsApp send through a Meta-approved template.
POST/v2/email/sendEmail send — content is an object with a subject.
POST/v2/voice/sendVoice call — spoken text or a pre-recorded audio_url.
POST/v2/otpIssue a verification code.
POST/v2/otp/verifyVerify a code.
GET/v2/contactsAddress book — read, create, update, delete.
GET/v2/contact-groupsContact groups, same operations.
GET/v2Contract discovery, without credentials.
GET/v2/pingConnectivity test, without credentials.

Webhooks and delivery receipts

A message’s status changes after the HTTP response. Rather than polling GET /v2/messages/{id}, declare a callback URL: it is called with POST on every state change.

The URL is set either on the account (back office, Integration & API) or per message through callback_url — which then takes precedence over the account one.

Payload received
{
  "id": "msg_2f1c9d4e8a6b",
  "channel": "sms",
  "status": "delivered",
  "to": "22960000000",
  "from": "FASTERMSG",
  "units": 1,
  "price": 12,
  "currency": "XOF",
  "client_reference": "cmd-42",
  "error": null,
  "sent_at": "2026-09-07T10:00:01.420Z",
  "delivered_at": "2026-09-07T10:00:06.900Z"
}

Statuses notified

The same six as on the message resource. Only delivered, undelivered and failed are terminal: after them, no further callback is to be expected for that message.

Retry policy

  • Your endpoint must answer 2xx in under ten seconds. Any other response, or an overrun, counts as a failure.
  • A failure is replayed with a growing interval. So answer 200 on receipt and process afterwards: slow processing always ends up being replayed.
  • Callbacks are not ordered. A sent can arrive after a delivered — trust the timestamps, never the order of arrival.
  • The same event may be delivered twice. Make your processing idempotent on the id + status pair.

Security

Never put a destructive action behind the callback URL, and serve it over HTTPS only.

Error codes

Every error follows the same shape, whatever the endpoint: a single object under error, which a client can tell apart from a success while knowing nothing about the call.

Shape of an error
{
  "error": {
    "type": "invalid_request",
    "code": "missing_parameter",
    "message": "A recipient is required.",
    "param": "to",
    "request_id": "2f1c9d4e-8a6b-4c1e-9f2a-7d3b5e0c1a84"
  }
}

type says how to react, code says what happened. Test code literally: it is stable. Never test message, which is written for developers and may be rephrased.

Error families

typeHTTPHow to react
invalid_request400 · 404 · 409Fix the call. Do not retry it unchanged.
authentication_error401Credentials missing, wrong or revoked.
permission_error403Authenticated, but the key lacks the required scope.
business_error402 · 403Business refusal: neither the call nor the credentials are at fault.
rate_limit_error429Retry after the Retry-After delay.
api_error500 · 501 · 503A failure on our side. Retryable, and never charged.

Common codes

codeHTTPCauseResolution
missing_parameter400A mandatory field is missing; param names it.Add the field reported.
invalid_parameter400Field present but unreadable: non-ISO date, unknown channel, content of the wrong type.Fix the format; see the channel’s example.
invalid_number400Recipient outside E.164 format, or not routable.Normalise to E.164 (8 to 15 digits).
unauthenticated401Basic or X-Api-Key missing, wrong, revoked, or the two with no affiliation between them.Check both credentials; regenerate the key if needed.
insufficient_scope403The key is not allowed to perform this action.Use a key of the account concerned.
insufficient_balance402Available balance too low, current holds deducted.Top up the paying account.
recipient_opted_out403The recipient removed themselves from the channel’s “Do not contact” list.Stop contacting them on this channel; the list is per channel.
sender_not_allowed403Sender ID unknown to the account, or not approved.Declare the sender and have it approved.
resource_not_found404Unknown identifier — or one belonging to another account.Check the id and the key’s account.
idempotency_conflict409The same Idempotency-Key was used for a different payload.Use a new key.
idempotency_in_progress409A request carrying this key is still in flight.Wait, then replay unchanged.
rate_limit_exceeded429Rate limit reached.Wait for Retry-After, then retry.
not_implemented501Endpoint declared in the contract, not served yet.Nothing to fix — follow us for its release.
service_unavailable503Temporary unavailability.Retry with a growing interval.
internal_error500Unexpected failure on our side.Retry, then quote us the request_id.

Rate limits

Throughput is capped per API key, over a sliding window. The default cap is 600 requests per minute; it can be raised on request, according to your volume.

Response headers

HeaderContent
X-RateLimit-LimitCap for the current window.
X-RateLimit-RemainingCalls left before refusal.
X-RateLimit-ResetUnix timestamp (seconds) when the window resets.
Retry-AfterSeconds to wait before retrying — present on a 429 only.

A well-behaved client paces itself on X-RateLimit-Remaining rather than waiting for the refusal. On a 429, honour Retry-After: retrying sooner only prolongs the block.

Other caps

  • 500 lines per batch send.
  • 24 h of retention for an Idempotency-Key; past that, the key is reusable and the call goes through again.
  • 10 s for a webhook endpoint to answer.