Every eSMS SMS API error comes back with an HTTP status and a JSON envelope whose error.code is a stable, machine-readable string such as country_not_enabled or insufficient_balance. Branch on that code, not on the message text: this guide lists the SMS API error codes you are likely to meet, what each means, how to fix it, and which are safe to retry. Refused sends are never charged.

Last reviewed: 29 October 2026

How to handle SMS API error codes

  1. Read the error envelope

    Errors share one shape. error.code is what you branch on, error.message is for people, and error.details carries extra context such as required and available on a 402. The older detail field is kept for backward compatibility.

    HTTP/1.1 403 Forbidden
    X-Request-Id: 3f9a1c2b4d5e6f708192a3b4c5d6e7f8
    
    {
      "error": {
        "code": "country_not_enabled",
        "message": "Sending to Cameroon (CM) is not enabled for this account yet. ...",
        "request_id": "3f9a1c2b4d5e6f708192a3b4c5d6e7f8",
        "details": { "to": "+237677123456", "country": "CM", "enable_url": "https://sms.esmsafrica.io/countries" }
      },
      "detail": { "code": "country_not_enabled", "...": "..." }
    }
  2. Decide: retry or fix

    Treat 429 and 5xx as temporary and retry with backoff. Treat other 4xx codes as something to fix: retrying the same request gives the same answer. The official SDKs already retry the temporary cases for you.

  3. Retry with the same Idempotency-Key

    Send an Idempotency-Key header and reuse it on every retry. If the first attempt actually went through, the retry returns the original result instead of sending and charging again. A plain HTTP example:

    async function sendWithPolicy(payload, idempotencyKey) {
      for (let attempt = 0; attempt < 4; attempt++) {
        const res = await fetch("https://sms.esmsafrica.io/api/messages/send", {
          method: "POST",
          headers: {
            "Authorization": `Bearer ${process.env.ESMS_API_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": idempotencyKey,   // same key on every attempt
          },
          body: JSON.stringify(payload),
        });
        if (res.ok) return res.json();
    
        const body = await res.json().catch(() => ({}));
        const code = body.error?.code;
        const requestId = res.headers.get("X-Request-Id");
    
        if (res.status === 429 || res.status >= 500) {      // temporary: back off, retry
          await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
          continue;
        }
        // 4xx: retrying will not help, fix the cause
        throw Object.assign(new Error(body.error?.message), { status: res.status, code, requestId });
      }
      throw new Error("Gave up after retries");
    }
  4. Log the request id

    Every response, success or error, carries X-Request-Id (also in error.request_id). Store it with the failure; it is the fastest way for support to find the exact call. You can also send your own X-Request-Id and it is echoed back.

SMS API error codes reference

The status shown is the HTTP status the API returns for that code today.

HTTPCodeMeaningFix
401unauthorizedThe API key is missing, invalid or revoked (message Invalid API key or Not authenticated).Send Authorization: Bearer esms_live_... with an active key.
403insufficient_scopeThe key lacks the permission for this call, such as send.Create a key with the right Permissions ticked.
403role_forbiddenThe key was made by a team member whose role cannot do this.Use a key made by someone with a suitable role.
403account_suspendedThe account is suspended or paused.Contact support.
400invalid_phoneThe to value is not a usable number.Send digits in international format.
400missing_country_codeA local number such as 0712... with no country code.Add the country code; the message suggests the right form.
400invalid_phone_lengthToo many or too few digits for that country.Use expected_digits in details to correct it.
400destination_unavailableLive SMS to that country is not available yet (Coming soon or on request). Not charged.Wait for it to open or contact sales; test keys can simulate it.
403country_not_enabledLive sends to that country are not switched on for your account. Not charged.Click Enable on the Countries page.
403country_blockedeSMS Africa has disabled that country for your account.Contact support.
403dashboard_onlyYou tried to enable a country with an API key.Enable countries in the dashboard (owner or admin).
400country_needs_approvalYou tried to self-enable a country outside Africa.Contact sales to approve it.
403sender_not_approvedThe sender ID is not approved for the destination country.Omit the sender ID or request it on the Sender IDs page.
403recipient_opted_outThe number is on your opt-out list, for example because it replied STOP.Do not send; remove it from the list only if the person opts back in.
402insufficient_balanceYour balance does not cover the send.Top up; details.required and details.available show the gap.
400missing_merge_fieldA placeholder has no value and no fallback for some recipients. Nothing sent.Add a fallback like {{name|Customer}} or fix the data.
409delivery_being_confirmedYou tried to retry a message we are still confirming with the network (status unknown, Confirming in the dashboard).Wait until it shows sent or failed; a failed one is refunded automatically.
409duplicate_messageSame text to the same number within 5 minutes, with duplicate protection on.Expected protection; change the text or wait.
409idempotency_in_progressA request with the same key is still running.Retry shortly with the same key.
413message_too_longThe message is over 10 segments.Shorten it.
422validation_errorThe body failed schema checks; details.errors lists the fields.Fix the listed fields.
422route_type_mismatchThe content does not match your account's route type (for example marketing text on an OTP-only account).Send matching content or ask support about your route type.
429rate_limitedPer-key limit exceeded: 20 single sends a second, 10 bulk or batch calls a minute.Back off and retry; spread the load.
429too_many_requestsMore than 5 Verify starts for one number in an hour.Ask the user to wait.
502otp_send_failedA Verify code could not be sent. Not charged.Safe to retry.
500internal_errorUnexpected server error.Retry with backoff; quote the request id if it persists.

Common errors and how to fix them

What you seeWhat it meansHow to fix it
Same 403 on every retry4xx errors are not temporary.Read error.code and fix the cause instead of retrying.
error.code is missingYou read the old detail field, which can be a plain string.Read error.code, which every error has.
Retry sent a second SMSThe retry had no Idempotency-Key, or a new key each time.Generate one key per logical send and reuse it on every attempt.
402 with plenty of balance shownThe balance is in a different currency or the multi-segment message costs more than expected.Compare details.required and details.available, and check segments.
Lots of 429s at peak timeYou are over the per-key rate limit.Use the bulk or batch endpoints for lists instead of looping single sends.

Frequently asked questions

Am I charged when a send is refused?

No. Requests refused with an error are not charged, and a message that later fails at the network is refunded.

Which errors should I retry?

429 and 5xx, with backoff and the same Idempotency-Key. Fix 4xx errors instead of retrying them.

Why do I get 402 instead of 400 for low balance?

402 Payment Required is used for insufficient_balance so your code can catch it separately and prompt a top-up.

Where do failed calls show up?

Every API-key call is listed on the Request Logs page under DEVELOPERS, with its status and request id.

Ready? Create a free eSMS account, then create a test API key and send your first simulated message in minutes. Nothing is charged until you switch to a live key.