Handling errors

|

View as Markdown

Infobip APIs return three kinds of codes:

  • HTTP status codes (for example, 200 OK, 404 Not Found) on every API response.
  • API status codes (for example, PENDING_ACCEPTED, DELIVERED_TO_HANDSET) in message responses, delivery reports, and message logs. They describe where a message is in its lifecycle. See Message status codes.
  • Delivery error codes (for example, NO_ERROR, EC_UNKNOWN_SUBSCRIBER) in message responses and delivery reports. They describe why a message failed, or confirm that nothing went wrong. See the Error code reference.

Errors reach your code by two separate paths, and each needs its own handling:

  • Request errors: The API rejects your request. You get them synchronously, as an HTTP 4xx or 5xx response with an error response body, in the code that made the request.
  • Network and delivery error codes: The API accepted your request, but the message failed later at the network level. You get them asynchronously, in a delivery report sent to your webhook, sometimes long after the request.


Request errors

Start with the HTTP status code. It shows whether the problem is in the request or on the Infobip side, and whether a retry can succeed.


HTTP status codes [#http-status-codes-request-errors]

The following table shows what each HTTP status code means and what to do.

StatusMeaningWhat to do
2xxThe request succeeded.Nothing. For send requests, success means the message was accepted, not delivered. Watch the delivery report.
400 Bad RequestThe request is malformed or does not match the request model of the endpoint.Read violations in the error response body, fix the request, then send it. See 400 Bad Request errors for common causes.
401 UnauthorizedThe credentials are missing or invalid.Make sure the request includes the Authorization header with valid credentials. See Authentication errors.
403 ForbiddenThe credentials are valid but lack permission for this resource.Use credentials with the required scope. See Authorization errors.
404 Not FoundThe endpoint or the requested resource does not exist.Compare the endpoint path with its API reference page, and make sure the resource ID in the request is correct.
429 Too Many RequestsToo many requests reached the endpoint within its rate limit.Retry with exponential backoff and a retry limit. See Retry safely.
Other 4xxSomething else is wrong with the request.Fix the request. Sending the exact same request again immediately returns the same error.
5xxThe request is valid, but an error occurred on the Infobip side.The error might be temporary, so a later retry can succeed. For send requests, find out whether the message was accepted before you retry. See Retry safely.

Error response body [#error-response-body-request-errors]

Unified API endpoints return the unified error model. The following example shows a 401 Unauthorized response:

Unified error
1{
2 "errorCode": "E401",
3 "description": "The request lacks valid authentication credentials for the requested resource.",
4 "action": "Check the resources and adjust authentication credentials.",
5 "violations": [],
6 "resources": [
7 { "name": "API Authentication", "url": "{apiAuthenticationDocsUrl}" },
8 { "name": "API endpoint documentation", "url": "{endpointDocsUrl}" }
9 ]
10}

The following table describes the fields of the unified error model:

FieldDescription
errorCodeAn error code that identifies the error case, for example E401.
descriptionA description of the error.
actionThe action to take to recover from the error.
violationsThe request properties that caused the error. Each entry has property (the request property) and violation (why it failed).
resourcesDocumentation to help you recover from the error. Each entry has name and url.

Handle errors by HTTP status code and error identifier. Do not match on the text of description, which can change. The API reference of each endpoint defines the fields, for example Send SMS message.

For a 400 Bad Request, each entry in violations names the request property that failed and why. Fix those properties before you send the request again.


Legacy error response body [#legacy-error-response-body-request-errors]

Some endpoints return the legacy error model:

Legacy error
1{
2 "requestError": {
3 "serviceException": {
4 "messageId": "UNAUTHORIZED",
5 "text": "Invalid login details"
6 }
7 }
8}

Your code can tell the two error models apart by their top-level property:

Top-level propertyModelError identifierExplanation
errorCodeUnifiederrorCodedescription
requestErrorLegacyrequestError.serviceException.messageIdrequestError.serviceException.text

The legacy error model has no equivalent of action, violations, or resources. In the legacy error model, messageId is an error identifier, not the ID of a message. As with description, do not match on the text of text.



Network and delivery error codes

When the API accepts a message, the outcome arrives later in a delivery report, sent to your webhook. To set up the webhook, see Webhooks. For the full delivery report schema, see Receive outbound SMS message report, or the equivalent page for your channel.

Each entry in results carries two objects:

  • status: The current or final state of the message, for example DELIVERED, UNDELIVERABLE, or REJECTED. See Message status codes.
  • error: The reason for the status. On success, name is NO_ERROR.

The following example shows a delivery report for a delivered message:

Delivery report (delivered)
1{
2 "results": [
3 {
4 "bulkId": "BULK-ID-123-xyz",
5 "messageId": "2250be2d4219-3af1-78856-aabe-1362af1edfd2",
6 "to": "41793026727",
7 "status": {
8 "groupId": 3,
9 "groupName": "DELIVERED",
10 "id": 5,
11 "name": "DELIVERED_TO_HANDSET",
12 "description": "Message delivered to handset"
13 },
14 "error": {
15 "groupId": 0,
16 "groupName": "OK",
17 "id": 0,
18 "name": "NO_ERROR",
19 "description": "No Error",
20 "permanent": false
21 }
22 }
23 ]
24}

When delivery fails, status moves to a failure group and error carries the reason:

Delivery report (failed, error object only)
1{
2 "error": {
3 "groupId": 1,
4 "groupName": "HANDSET_ERRORS",
5 "id": 1,
6 "name": "EC_UNKNOWN_SUBSCRIBER",
7 "description": "Unknown subscriber",
8 "permanent": true
9 }
10}

The field that decides what you do next is permanent. true means sending the same message again, unchanged, fails the same way. false means the condition might be temporary, so a later resend can succeed. The status object can also carry an action field with a suggested next step.

NOTEGroup IDs mean different things in different products. In SMS, group 1 is handset errors. In Email, it is dropped messages. In Chat, it is client errors. Do not switch on groupId alone. Use name, and look up codes in the Error code reference under the product that sent the message.

Act on a delivery error [#act-on-a-delivery-error-network-and-delivery-error-codes]

  1. Start with status. Act only on final statuses, and ignore intermediate ones such as the PENDING group. DELIVERED_TO_HANDSET is final and needs no action. For which statuses are final, see Message delivery lifecycle.
  2. Read error.name for the reason. Look it up in the Error code reference for the recommended fix.
  3. Use permanent to decide whether to resend.
    • true: Do not resend the same message. Fix the cause first, such as the destination number, sender, or template. If the cause cannot be fixed, stop sending to that recipient.
    • false: A later resend can succeed. A resend is a new message, with a new messageId, and Infobip charges for it again. Wait between attempts rather than resending immediately.

The following example applies these rules in a delivery report handler. queue_resend and flag_for_review stand for your own logic.

Delivery report handler
1def handle_delivery_report(report):
2 for result in report["results"]:
3 if result["status"]["groupName"] in ("PENDING", "DELIVERED"):
4 continue
5 
6 error = result["error"]
7 if error["permanent"]:
8 # Resending unchanged fails the same way. Fix the cause first.
9 flag_for_review(result["messageId"], result["to"], error["name"])
10 else:
11 # A resend is a new message with a new messageId.
12 queue_resend(result["messageId"], result["to"], error["name"])

Return HTTP 200 from your webhook endpoint as soon as the report arrives, and process the report asynchronously. If your endpoint is unavailable, Infobip pushes the same report again. Make sure your handler does not queue the same resend twice. See Push retry cycle.



Retry safely

Retry a request only when the error can be temporary: 429, 5xx, or a timeout. For delivery errors with permanent: false, see Act on a delivery error.

  • 429 Too Many Requests: An endpoint returns 429 when usage exceeds its rate limit. Most rate limits apply to non-message-sending APIs, but some send endpoints also have them, for example Email. Retry with exponential backoff and a limit on the number of retries, and monitor how many requests are throttled. See Handling throttled requests.
  • 5xx or a timeout: The error might be temporary, but for a send request you cannot be sure whether the message was accepted. Find out before you retry.
IMPORTANTRetrying a send request can deliver the message twice. A send request can time out, lose its connection, or return a 5xx. In each case, you cannot be sure whether the message was accepted. Infobip has no public idempotency key, so the platform cannot recognize a retried send request as a duplicate. The messageId field on a send request is for tracking only and does not deduplicate retries. Before you retry, look up the message in the message logs or wait for a delivery report.

The following example retries 429 responses with exponential backoff. It treats an unknown outcome as a signal to look up the message before you retry. The 429 branch applies to any endpoint with a rate limit, and the unknown-outcome branch applies to send requests.

Retry with backoff
1import time
2 
3import requests
4 
5MAX_RETRIES = 5
6 
7class RequestRejected(Exception):
8 """4xx: fix the request. Sending it unchanged fails again."""
9 
10class OutcomeUnknown(Exception):
11 """Timeout or 5xx: the message may have been accepted. Look it up before retrying."""
12 
13class Throttled(Exception):
14 """429 after every retry: reduce the request rate, or contact Support."""
15 
16def error_code(response):
17 try:
18 body = response.json()
19 except ValueError:
20 return None
21 if "errorCode" in body:
22 return body["errorCode"]
23 return body.get("requestError", {}).get("serviceException", {}).get("messageId")
24 
25def send(url, api_key, payload):
26 headers = {"Authorization": f"App {api_key}"}
27 for attempt in range(MAX_RETRIES):
28 try:
29 response = requests.post(url, headers=headers, json=payload, timeout=30)
30 except requests.exceptions.RequestException as exc:
31 raise OutcomeUnknown("No response received") from exc
32 
33 if response.status_code == 429:
34 time.sleep(2 ** attempt)
35 continue
36 if response.status_code >= 500:
37 raise OutcomeUnknown(f"{response.status_code} {error_code(response)}")
38 if response.status_code >= 400:
39 raise RequestRejected(f"{response.status_code} {error_code(response)}")
40 return response.json()
41 
42 raise Throttled("Still throttled after the maximum number of retries")


Contact Support

Before you contact Support, check the message in Message logs, check Operator connectivity status for outages, and for REJECTED_FLOODING_FILTER, see Flooding filter (DCL).

If the cause is still unclear, contact Support. Do not include your API key or other credentials. So that Support can find your traffic, include the following:

  • The endpoint and HTTP method you called
  • The time of the request, with time zone
  • The HTTP status code and the full response body
  • The messageId and bulkId, for message requests
  • The full delivery report, for delivery errors


Related pages

Error code reference
Look up network, delivery, and product-specific error codes across SMS, Voice, MMS, Email, Chat, and more.

Message status codes
The API status codes returned in message responses, delivery reports, and logs.

Rate limits and throttling
How rate limits and throttling work, and how to handle 429 responses with retry logic.




Was this page helpful?