SendComms
API Reference

Error Handling

SendComms uses conventional HTTP response codes to indicate the success or failure of an API request. Codes in the 2xx range indicate success, codes in the 4xx range indicate client errors, and codes in the 5xx range indicate server errors. Every error carries a stable machine-readable code and a docs_url that links straight back to this page.

Error Response Structure

All error responses follow a consistent structure. Any extra context an error carries is flattened alongside code and message — it is never nested under a details object.

ERROR RESPONSE FORMAT
{
  "success": false,             // always false on an error
  "error": {
    "code": "ERROR_CODE",       // machine-readable, stable
    "message": "Human-readable description",
    "transaction_id": "sms_...", // when the request reached a transaction
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}

Fields you can rely on

  • code andmessage are present on every error.
  • docs_url is present on every error. Codes documented on this page link directly to their section anchor; anything else links to the top of this page.
  • transaction_id (orbatch_id) appears once a transaction record exists — log it, it is what support needs to trace the request.
  • error_id appears on unexpected 503s and identifies the recorded incident on our side.
  • Context fields vary by code: required andcurrency on balance errors,limit/remaining/reset/retryAfter on rate limits,errors on validation failures, andprovider_message on declined purchases.

HTTP Status Codes

CodeStatusDescription
200OKRequest succeeded
201CreatedData or airtime purchase created
400Bad RequestInvalid parameters, or the provider rejected this specific message
401UnauthorizedInvalid or missing API key
402Payment RequiredInsufficient account balance
403ForbiddenAccount suspended
404Not FoundTransaction or resource not found
409ConflictA request with the same idempotency_key is still in progress
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error
502Bad GatewayCould not reach the upstream data or airtime provider
503Service UnavailableService temporarily unavailable or not configured

Client Error Codes

These errors mean something about the request needs to change. Retrying an identical request will fail identically, so fix the request first. All validation runs before any balance is reserved or any rate-limit or idempotency state is consumed — a rejected request never costs you anything.

Authentication and access

Raised before the request body is even read.

StatusCodeWhen it happensHow to fix it
401UNAUTHORIZEDThe Authorization header is missing or malformed, or the key is unknown, revoked or inactive.Send Authorization: Bearer <your_api_key> (the legacy X-API-Key header also works) and confirm the key is still active in your dashboard.
403ACCOUNT_SUSPENDEDThe API key is valid but the account it belongs to has been deactivated.Contact support@sendcomms.com. Requests keep failing until the account is reactivated.
403FORBIDDENThe API key is valid, but it does not carry the permission for the service you called. The response names the needed scope in required_permission and the key’s current scopes in key_permissions.Use a key that has the service enabled, or create a new key with that permission from the dashboard.
403SENDER_ID_NOT_YOURSThe alphanumeric sender ID passed as "from" is registered to another SendComms account. Sender IDs are globally unique so one customer cannot send under another business’s brand.Register your own sender ID under Sender IDs, omit "from" to use the default sender, or contact support@sendcomms.com if you own the brand.
409CONFLICTA domain you tried to add is already held by another SendComms account, is already on your account, or is already configured on our mail infrastructure. The response carries a reason field: taken, taken_by_you or in_use_upstream. Creating a mailbox on a domain that has not been verified yet also returns 409.Call GET /api/v1/domains/check?name=… first. If you own a domain that shows as taken, contact support@sendcomms.com. For mailboxes, verify the domain before creating addresses on it.

Request format

The body could not be parsed, or is missing something the endpoint requires.

StatusCodeWhen it happensHow to fix it
400INVALID_JSONThe request body could not be parsed as JSON at all (empty body, trailing comma, form encoding).Send a JSON body with Content-Type: application/json. Serialise with your language’s JSON encoder rather than string concatenation.
400INVALID_REQUESTThe body is valid JSON but not the shape the endpoint expects — a field has the wrong type, or the body is not a JSON object.Read the errors array included in the error object: it lists the offending field path and the expected type.
400MISSING_FIELDA required field is absent — to, message, subject, phone_number, network, capacity_gb, or the transaction_id query parameter on status endpoints. In /email/batch the message names the offending index.Add the field named in the message. The message is always of the form "Missing required field: <name>".
400MISSING_CONTENTAn email was submitted with neither html nor text content.Provide at least one of html or text. Sending both is recommended so clients that block HTML still render the message.
400NO_RECIPIENTSThe to field is present but resolves to an empty list.Pass a non-empty string or a non-empty array of addresses in to.
400TOO_MANY_RECIPIENTSMore than 50 recipients on a single email. /email/send counts to; the mail service also rejects any message whose to + cc + bcc together exceed 50.Split the send, or use POST /api/v1/email/batch to submit up to 100 separate messages in one call.
400BATCH_LIMIT_EXCEEDEDPOST /api/v1/email/batch was called with more than 100 emails in the emails array.Chunk the array into batches of 100 or fewer and send them sequentially.
400INVALID_INPUTPOST /api/v1/email/batch was called without an emails array, or with an empty one.Send { "emails": [ ... ] } with at least one message object.
404NOT_FOUNDA status lookup referenced a transaction_id that does not exist, or does not belong to your account.Use the transaction_id returned by the original send/purchase response. IDs are scoped to the account that created them.

Field validation

The request is well formed, but a value is not something SendComms can send.

StatusCodeWhen it happensHow to fix it
400INVALID_EMAILAn address in to, cc or bcc is not a valid email address. The message repeats the address, and names the field for cc/bcc.Validate addresses before sending. Display names must use the Name <user@example.com> form.
400UNVERIFIED_SENDER_DOMAINThe domain in the from address is not a verified sending domain on your account. The error echoes provided_from and provided_domain.Verify the domain under Domains in your dashboard, or omit from entirely to send from your account default sender. SendComms never silently substitutes a different sender.
400INVALID_PHONE_NUMBERThe SMS or airtime recipient is not a valid E.164 number.Send the number in E.164 format, including the + and country code — for example +233540800994.
400MESSAGE_TOO_LONGAn SMS body is longer than 1600 characters (10 concatenated segments).Shorten the message, or split it into several sends. Non-GSM characters shorten each segment, so long unicode messages hit the limit sooner.
400INVALID_CONTINENTThe optional continent routing hint on /sms/send is not one of the supported values.Use one of the values listed in the error message, or omit continent and let SendComms detect it from the number.
400INVALID_PHONEThe number on a Ghana data purchase is not a valid Ghanaian mobile number.Use 0241234567 or +233241234567. Data bundles are Ghana-only, so international numbers are rejected here.
400INVALID_NETWORKThe network on a data purchase is not a supported Ghanaian carrier.Use mtn, telecel or airteltigo (case-insensitive).
400INVALID_AMOUNTThe airtime amount is missing, zero, negative or not numeric.Send amount as a positive number in the operator’s currency.
400INVALID_OPERATORThe airtime operatorId could not be read as a number.Pass the numeric operator id from the operator lookup endpoint, not the operator name.
400INVALID_COUNTRYThe optional airtime countryCode is not a two-letter ISO code.Use a 2-letter ISO 3166-1 alpha-2 code such as GH or NG, or omit the field.
400INVALID_URLWebhook registration was sent without a url, or with a url that is not HTTPS.Register an absolute https:// endpoint. Plain HTTP endpoints are rejected.
400INVALID_EVENTSWebhook registration was sent with an empty events array, or with event names SendComms does not emit.Send a non-empty array of supported event names — the error message lists every valid value — or use "*" to subscribe to all of them.
400MISSING_IDDELETE /api/v1/webhooks was called without the id query parameter.Append ?id=<webhook_id>, using the id returned when the webhook was registered.

Billing, limits and concurrency

Your request is valid, but your account state or request rate blocks it right now.

StatusCodeWhen it happensHow to fix it
402INSUFFICIENT_BALANCEA prepaid account does not have enough balance to cover the request. Balance is reserved before the provider is called, so nothing is sent.Top up in the dashboard. The error carries required and currency so you can alert on the shortfall.
429RATE_LIMIT_EXCEEDEDYou exceeded a per-minute, per-hour, per-day or per-month limit for your plan, either globally or for one service.Back off and retry after retryAfter seconds. The response also carries Retry-After and X-RateLimit-* headers.
409REQUEST_IN_PROGRESSAnother request with the same idempotency_key is still in flight for this account.Wait and retry the same key — once the first request finishes, replaying the key returns its stored response with X-Idempotent-Replay: true. Use a fresh key for a genuinely new message.

Examples

401UNAUTHORIZED
Invalid or missing API key
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or missing API key",
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}
400MISSING_FIELD
A required field is missing
{
  "success": false,
  "error": {
    "code": "MISSING_FIELD",
    "message": "Missing required field: to",
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}
400INVALID_REQUEST
Body parsed, but failed validation
{
  "success": false,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Invalid request body",
    "errors": [
      {
        "type": "string_type",
        "loc": ["message"],
        "msg": "Input should be a valid string"
      }
    ],
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}
400UNVERIFIED_SENDER_DOMAIN
The from domain is not verified on your account
{
  "success": false,
  "error": {
    "code": "UNVERIFIED_SENDER_DOMAIN",
    "message": "The \"from\" domain \"example.com\" is not verified for your account. Verify it under Domains, or omit \"from\" to use your default sender.",
    "provided_from": "Acme <hello@example.com>",
    "provided_domain": "example.com",
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}
402INSUFFICIENT_BALANCE
Not enough prepaid balance to cover the request
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Insufficient balance. Please add funds to your account.",
    "required": 0.035,
    "currency": "USD",
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}
429RATE_LIMIT_EXCEEDED
Too many requests for your plan
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Please try again later.",
    "limit": 5,
    "remaining": 0,
    "reset": 1766452500,
    "retryAfter": 39,
    "docs_url": "https://docs.sendcomms.com/docs/errors#client-error-codes"
  }
}
409REQUEST_IN_PROGRESS
The same idempotency_key is still in flight
{
  "success": false,
  "error": {
    "code": "REQUEST_IN_PROGRESS",
    "message": "Request is being processed. Please wait.",
    "docs_url": "https://docs.sendcomms.com/docs/errors"
  }
}
503SMS_SEND_FAILED
The carrier is failing — safe to retry
{
  "success": false,
  "error": {
    "code": "SMS_SEND_FAILED",
    "message": "Failed to send SMS. Please try again in a few minutes.",
    "transaction_id": "sms_mfa1x2y_9c4d1e2f3a4b",
    "docs_url": "https://docs.sendcomms.com/docs/errors#service-error-codes"
  }
}

Service Error Codes

These errors come from the carriers, mail infrastructure and top-up providers behind SendComms, or from SendComms itself. When a send or purchase fails, any reserved balance is refunded and the idempotency lock is released before the error is returned — so the same key can be retried safely.

StatusCodeServiceDescriptionRetryable
400503
SMS_SEND_FAILEDSMS400 when the carrier rejected this specific message (unverified sender ID, blocked or opted-out recipient) — the message explains what to change. 503 when the carrier itself is failing.On 503
400503
EMAIL_SEND_FAILEDEmail400 when the mail service rejected the message and returned a reason. 503 when the mail service is unreachable or erroring.On 503
400503
PURCHASE_FAILEDData / AirtimeThe provider declined the purchase. 400 carries provider_message with the exact reason (out of stock, invalid bundle, operator rejected). 503 means the provider is degraded.On 503
503
DATA_PURCHASE_FAILEDDataAn unexpected failure inside the data purchase flow. The reservation is refunded and the idempotency lock released before the error is returned.Yes
503
AIRTIME_PURCHASE_FAILEDAirtimeAn unexpected failure inside the airtime purchase flow. The reservation is refunded and the idempotency lock released before the error is returned.Yes
502
PROVIDER_ERRORData / AirtimeSendComms could not reach the upstream provider at all (connection failure or timeout). Nothing was charged.Yes
503
SERVICE_UNAVAILABLEAllThe requested service is not currently available on this deployment — for example the email, data or airtime provider is not configured.No
500
INTERNAL_ERRORAllAn unhandled error. The message is always "Internal server error" — details are logged on our side, never returned.Yes

Reading a 400 vs a 503 on the same code

SMS_SEND_FAILED, EMAIL_SEND_FAILED and PURCHASE_FAILED use the HTTP status to tell you what to do. A 400 means the provider rejected this particular message and the message text says why — retrying unchanged will fail again. A 503 means the provider is having trouble: wait a few seconds and retry, reusing the same idempotency_key so you cannot double-send.

Best Practices

1

Check the Error Code

Use the error.code field for programmatic handling rather than parsing error messages. Message wording can change; codes do not.

2

Use Idempotency Keys for Retries

Always include an idempotency_key when retrying failed requests to prevent duplicate processing. A retry that arrives while the first attempt is still running returns REQUEST_IN_PROGRESS (409); one that arrives after it finished replays the original response with an X-Idempotent-Replay: true header.

3

Implement Exponential Backoff

For rate limit (429), bad gateway (502) and service unavailable (503) errors, implement exponential backoff starting with a 1-second delay and doubling each retry. On a 429, prefer the Retry-After header when it is present.

4

Log Transaction IDs

Always log the transaction_id, batch_id or error_id from error responses. This helps support quickly identify and resolve issues.

5

Follow the docs_url

Every error includes a docs_url pointing at the section of this page that explains it. Surfacing that link in your own logs and dashboards saves your team the lookup.

Retry Example

Here's how to implement safe retries with idempotency:

JAVASCRIPT
async function sendSMSWithRetry(to, message, maxRetries = 3) {
  const idempotencyKey = crypto.randomUUID();

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const response = await fetch('https://api.sendcomms.com/api/v1/sms/send', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_API_KEY',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ to, message, idempotency_key: idempotencyKey })
    });

    const data = await response.json();
    if (response.ok) return data; // Success

    const code = data.error?.code;

    // 409: the first attempt is still running - wait, then retry the SAME key
    // 429/502/503: transient - back off and retry
    const retryable =
      code === 'REQUEST_IN_PROGRESS' ||
      response.status === 429 ||
      response.status === 502 ||
      response.status === 503;

    // Anything else (validation, balance, unverified sender) will fail
    // identically on retry - fix the request instead.
    if (!retryable || attempt === maxRetries) {
      throw new Error(`${code}: ${data.error?.message} (${data.error?.docs_url})`);
    }

    const retryAfter = Number(response.headers.get('Retry-After'));
    const delay = retryAfter ? retryAfter * 1000 : Math.pow(2, attempt) * 1000;
    await new Promise(r => setTimeout(r, delay));
  }
}