Error codes
The API returns one error shape everywhere, with a stable code and a human-readable message. Thirty-nine codes exist. They are all listed below, with what to do about each one.
The shape of an error
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "TEXT_TOO_LONG",
"message": "Le texte dépasse 1600 caractères"
}- Branch on
code, never onmessage. The code is part of the contract; the message is prose for a human reader, it is written in French today, and it can change at any time. - If you surface errors to your own users, map the code to a sentence of your own, in their language. That is exactly what this console does.
- The HTTP status tells you the family; the code tells you the cause. Both are stable.
One response breaks this shape, and it is deliberate: a duplicate
clientReference on POST /v1/sms returns 409 with the existing message, not with an error object. Handle it as a success. Idempotency explains why. HTTP statuses at a glance
| Status | Meaning | Retry? |
|---|---|---|
| 400 | The request is wrong. | No — fix the request first. |
| 401 | No usable API key. | No. |
| 402 | Not enough credit. | After a top-up. |
| 403 | The account is not allowed to send. | No. |
| 404 | Unknown, or not yours. | No. |
| 409 | Conflict. On a send, this is the duplicate reference case. | No — and on a send, nothing failed. |
| 429 | Too fast, or daily quota reached. | Yes, after Retry-After. |
| 500 | Our fault. | Yes, with exponential backoff. |
| 503 | A dependency is switched off or unreachable. | Yes, after Retry-After. |
Retry-After is sent as a number of seconds on 429 RATE_LIMITED (1 s) and on 503 OPERATOR_DISABLED (3600 s). When the header is absent, use your own backoff.
Sending an SMS
The codes a /v1 integration actually meets. Everything else below comes from the console.
| Code | Status | Meaning | What to do |
|---|---|---|---|
| INVALID_API_KEY | 401 | The key is missing, malformed, unknown or revoked. | Send the header as "Authorization: Bearer gsk_…". If the key was revoked, create a new one in the console. |
| ACCOUNT_SUSPENDED | 403 | The account behind the key is no longer active. | Contact support. Retrying will not help. |
| TEXT_EMPTY | 400 | The text is missing, empty or only whitespace. | Check the variable you interpolate into the message before sending. |
| TEXT_TOO_LONG | 400 | The text is longer than 1600 characters. | Shorten the message, or split it yourself into several sends. |
| INVALID_NUMBER | 400 | The number is not a Cameroonian mobile number. | Accepted formats: +2376XXXXXXXX, 2376XXXXXXXX, 6XXXXXXXX. Validate at input time, not at send time. |
| PRICE_NOT_CONFIGURED | 400 | No price is set for the operator this number resolves to. | Nothing you can fix from the client side: report it, it is a configuration gap. |
| OPERATOR_DISABLED | 503 | Sending to that network is temporarily switched off. Nothing was charged. | Honour the Retry-After header (3600 seconds) and queue the message on your side. |
| QUOTA_EXCEEDED | 429 | The daily quota of the account is reached. | Retry after midnight UTC, or ask for a higher quota. Disabled by default. |
| RATE_LIMITED | 429 | More than ten requests per second on this key. | Honour Retry-After (1 second) and space out your loop. |
| INSUFFICIENT_BALANCE | 402 | The balance does not cover the price of this message. Nothing was queued. | Top up in the console. Watch balanceAfterXaf on each send to top up before you run dry. |
| NOT_FOUND | 404 | Unknown identifier — or a message that belongs to another application. | Check the messageId and the key you used. The API never answers 403 here, on purpose. |
| BAD_REQUEST | 400 | Malformed JSON, or a parameter of the wrong type. | Check the Content-Type header and the body you actually sent, not the one you meant to send. |
| VALIDATION_ERROR | 400 | A field failed validation; the message names the field. | Read the message field: it is the only place the offending field appears. |
| INTERNAL_ERROR | 500 | Unexpected failure on our side. | Retry with exponential backoff. If it persists, report it with the time of the call. |
Account and sign-in
Returned while creating an account, verifying a number or signing in.
| Code | Status | Meaning | What to do |
|---|---|---|---|
| ACCOUNT_PENDING | 403 | The phone number of the account has not been verified yet. | Enter the code received by SMS to activate the account. |
| ACCOUNT_ALREADY_ACTIVE | 400 | The account is already verified. | Sign in instead of asking for another verification code. |
| INVALID_CREDENTIALS | 401 | Wrong email or password. | Reset the password from the sign-in screen. |
| INVALID_EMAIL | 400 | The email address is missing or malformed. | Provide a valid address. |
| PASSWORD_TOO_SHORT | 400 | The password is shorter than 8 characters. | Use at least 8 characters. |
| EMAIL_TAKEN | 409 | An account already exists with this email address. | Sign in, or recover the password. |
| PHONE_TAKEN | 409 | An account already exists with this phone number. | Sign in with that account, or use another number. |
| OTP_INVALID | 400 | The verification code does not match. | Check the digits, or request a new code. |
| OTP_EXPIRED | 400 | The verification code is past its validity window. | Request a new code. |
| OTP_MAX_ATTEMPTS | 400 | Too many wrong attempts on the same code. | Request a new code; the old one is burned. |
Applications, webhooks and credit
Returned by console operations on applications, webhook endpoints and top-ups.
| Code | Status | Meaning | What to do |
|---|---|---|---|
| INVALID_SLUG | 400 | Application slug outside the allowed shape. | Lowercase letters, digits and hyphens, 2 to 50 characters. |
| SLUG_TAKEN | 409 | Another application already uses this slug. | Pick a different one. The slug is what webhooks carry in applicationId. |
| INVALID_URL | 400 | The webhook URL does not start with http:// or https://. | Give an absolute URL. Use https:// — the payload carries phone numbers. |
| UNSUPPORTED_EVENT | 400 | The subscription asks for an event type that does not exist. | Only sms.sent exists today; it fires on every status change. |
| INVALID_AMOUNT | 400 | Top-up amount below the minimum, or not a positive number. | Check the minimum shown on the top-up screen. |
| TOPUP_PENDING | 409 | A top-up is already in progress on this account. | Finish or wait for the current one before starting another. |
| TOPUP_DISABLED | 503 | Online top-up is switched off. | Ask for a manual credit while it is unavailable. |
| CAMPAY_UNAVAILABLE | 503 | The mobile money provider did not answer. | Try again in a few minutes. No money was taken. |
Administration
Routing rules, prices and statistics filters, reserved for administrators.
| Code | Status | Meaning | What to do |
|---|---|---|---|
| INVALID_OPERATOR | 400 | Unknown operator in a routing rule. | Use ORANGE, MTN, CAMTEL, NEXTTEL or DEFAULT. |
| INVALID_PREFIX | 400 | Malformed prefix in a routing rule. | Digits only, starting with 6. |
| DUPLICATE_PREFIX | 400 | The same prefix appears twice in the submitted list. | Remove the duplicate line. |
| DUPLICATE_OPERATOR | 400 | The same operator appears twice in the submitted list. | Remove the duplicate line. |
| INVALID_PRICE | 400 | A negative price was submitted. | Prices are whole XAF, zero or more. |
| INVALID_STATUS | 400 | Unknown status in a filter or an account update. | Use one of the documented values. |
| INVALID_PERIOD | 400 | The start of the period is not before its end. | Swap from and until, or widen the window. |
Writing a client that survives
- Retry only 429, 500 and 503 — and only with backoff. Retrying a 400 in a loop simply sends the same wrong request faster.
- Always send a
clientReference. It is what makes a retry safe when you cannot tell whether the first call went through. - Log the
code, the HTTP status and your own reference. Those three are enough to reconstruct any incident. - Handle
402before it happens: readbalanceAfterXafon every send, or pollGET /v1/balance, and alert yourself early.
Ready to send your first SMS?
Create an account, get 100 FCFA of credit and an API key straight away.
Create account