Errors
Every error has a code that never changes. Branch on error.code, show error.message, and look the code up here.
The error body
A failed request answers with an HTTP status and an error object.
{
"error": {
"type": "billing_error",
"code": "E03001",
"name": "InsufficientBalance",
"message": "Insufficient account balance. Top up to continue sending.",
"doc_url": "https://docs.ponitor.com/api/errors#E03001",
"request_id": "2d017493-d307-4628-b9a3-068160069183"
},
"message": "Insufficient account balance. Top up to continue sending."
}
| Field | Always there | What it is |
|---|---|---|
type | Yes | The kind of error, from the list below, for branching broadly. |
code | Yes | E and five digits. It never changes and is never reused. Branch on this. |
name | Yes | A readable name for the code, such as InsufficientBalance, for your logs. |
message | Yes | What happened, to show or log. Don't parse it: the words can change. |
doc_url | Yes | The code's entry on this page. |
request_id | Yes | The request's id, also sent as the X-Request-Id header. Quote it when you contact support. |
param | No | The one field at fault, when only one is. |
details | No | On a validation error, every field at fault, each with its param and message. |
A request that fails validation lists each field in details. param is a dotted path: recipients.0 is the first recipient.
{
"error": {
"type": "validation_error",
"code": "E01001",
"name": "ValidationError",
"message": "The request has 2 problems.",
"doc_url": "https://docs.ponitor.com/api/errors#E01001",
"request_id": "c1f0b7a2-5e1d-4c8e-9a0b-7d6e5f4c3b2a",
"details": [
{ "param": "externalId", "message": "externalId is required" },
{ "param": "content", "message": "content must be 2 or more characters long" }
]
},
"message": "The request has 2 problems.",
"errors": [{ "message": "externalId is required" }, { "message": "content must be 2 or more characters long" }]
}
Fields from before codes
message beside error, and on some errors code (such as RECIPIENT_OPTED_OUT) and errors, are what errors carried before codes. They are deprecated and still sent, so code written against them keeps working. Use error.message, error.code and error.details in new code.
Types and statuses
type | Status | Meaning | Retry? |
|---|---|---|---|
validation_error | 400 | A field is missing or invalid. See details. | No. Fix the request first. |
bad_request_error | 400 | The request can't be done as sent: a sender that isn't yours, a number that isn't one, a body that isn't JSON. | No |
billing_error | 400 | The balance or the workspace's credit is too low, or prices aren't set up. | No. Add funds first, or contact support if prices aren't set up. |
auth_error | 401 | The API key is missing, wrong or revoked, or sent to the other base URL. | No. Fix the key or the base URL. |
permission_error | 403 | Your team hasn't completed compliance or is suspended, or the workspace is paused or wasn't given that product or sender. | No |
not_found_error | 404 | Your team has nothing with that id, or there's no endpoint at that path. | No |
conflict_error | 409 | The request clashes with the state things are in: a price that changed, a request still being processed, or something the sandbox doesn't do. | Only E01008 |
payload_too_large_error | 413 | The body is too large. | No |
unprocessable_error | 422 | The request is valid but can't be done, such as a marketing template to someone who unsubscribed. | No |
rate_limit_error | 429 | Too many requests in a minute. See Rate limits. | Yes, after a pause. |
internal_error | 500 | Something failed on our side. | Yes, with the same externalId. |
service_unavailable_error | 503 | A service Ponitor relies on didn't answer. Nothing was done. | Yes, shortly. |
A request that ends in a 4xx isn't charged. A retry after a 5xx or a timeout with the same externalId doesn't send or charge twice. See Retries and externalId.
Why a message wasn't delivered
A message that ends as EXPIRED, UNDELIVERED or FAILED has a failureCode (an E11 code), and a failedReason in plain words that you can show your users. They come with the message: in its webhook, and from Get a message.
{
"message": {
"id": "e2c38865-dc0a-4299-baef-c07a9bfa9c33",
"status": "EXPIRED",
"failureCode": "E11001",
"failedReason": "Recipient unreachable: phone was off or out of coverage until the message expired"
}
}
Branch on failureCode, not on failedReason. A message that failed before codes existed may have no failureCode.
Handling errors
This example retries what's safe to retry and logs the rest.
const response = await fetch('https://api.ponitor.com/messaging/v1/messages', {
method: 'POST',
headers: { apiKey: process.env.PONITOR_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (response.ok) {
const { message } = await response.json();
// 201: new message. 200: the original, for an externalId you'd already used.
} else {
const { error } = await response.json().catch(() => ({ error: undefined }));
if (response.status === 429 || response.status >= 500 || error?.code === 'E01008') {
// Wait, then send the same request again, with the same externalId.
} else if (error?.code === 'E03001') {
// Out of balance: top up, then send again.
} else {
// Fix the request or the key; sending it unchanged fails again.
console.error(response.status, error?.code, error?.message, error?.details, error?.request_id);
}
}
Every code
Each code links here from its doc_url. Codes are never renamed or reused; new ones may be added, so treat a code you don't know by its type.
Requests
The request itself: its body, its path, its rate.
| Code | Name | Status | Meaning | What to do | Retry? |
|---|---|---|---|---|---|
E01000 | InternalError | 500 | Something failed on our side. Nothing you sent caused it. | Send the same request again, with the same externalId so nothing is sent twice. If it keeps happening, contact support with the request_id. | Yes |
E01001 | ValidationError | 400 | The body was read, but one or more fields are missing or invalid. details lists each one. | Fix each field in details, then send the request again. | No |
E01002 | BadRequest | 400 | The request can't be done as sent. The message says why. | Change the request as the message says, then send it again. | No |
E01003 | MalformedJson | 400 | The body couldn't be read as JSON. | Send a JSON body, with the header Content-Type: application/json. | No |
E01004 | PayloadTooLarge | 413 | The body is over the size the endpoint takes. | Send a smaller body. Upload a file and send its link instead of the file. | No |
E01005 | NotFound | 404 | Your team has nothing with that id. | Check the id, and that you use the key of the team that owns it. | No |
E01006 | RouteNotFound | 404 | The path or the method is not one the API has. | Check the path and the method against the API reference. | No |
E01007 | Conflict | 409 | The request clashes with something that already exists or has changed. | Read the object again, then decide what to send. | No |
E01008 | RequestInProgress | 409 | A request with the same externalId is still being handled. | Wait a moment, then send the same request again. You get the original back. | Yes |
E01009 | NotInSandbox | 409 | The sandbox does without it: payments, Sender ID requests, and teams and keys, which are managed in Ponitor. | Do it in Ponitor, with a live key. | No |
E01010 | Unprocessable | 422 | Every field is right, but what it asks for is not possible now. | The message says why. Change what you ask for. | No |
E01011 | RateLimited | 429 | Your team sent more requests in a minute than its limit. Before codes: rate_limited. | Wait for the seconds in the Retry-After header, then send again. See Rate limits. | Yes |
E01012 | ServiceUnavailable | 503 | Something Ponitor relies on is down for a moment. Nothing was sent or charged. Before codes: unavailable. | Send the same request again shortly. | Yes |
Sign-in and access
The API key, the team, its compliance, and what a workspace may use.
| Code | Name | Status | Meaning | What to do | Retry? |
|---|---|---|---|---|---|
E02001 | Unauthenticated | 401 | The request has no API key or sign-in, or one Ponitor no longer accepts. | Send your API key in the apiKey header. | No |
E02002 | InvalidApiKey | 401 | The key is not one of your team's live keys. | Check that you copied the whole key. A key is shown only once: if you've lost it, generate a new one under Settings › Developers › API Keys. | No |
E02003 | KeyForOtherEnvironment | 401 | A sandbox key was sent to api.ponitor.com, or a live key to sandbox-api.ponitor.com. | Send sandbox keys to sandbox-api.ponitor.com and live keys to api.ponitor.com. | No |
E02004 | ApiKeyAtAppAddress | 401 | An API key was sent to the address the app uses. | Send the request to api.ponitor.com (live) or sandbox-api.ponitor.com (sandbox). | No |
E02005 | SessionExpired | 401 | The sign-in has expired or is not valid. | Sign in again. | No |
E02006 | TeamNotFound | 401 | The team the key or the sign-in belongs to no longer exists. | Use a key of a team that exists, or contact support. | No |
E02007 | PermissionDenied | 403 | You are signed in, but this action is not one you may take. | Ask an owner of the team for access. | No |
E02008 | RoleNotAllowed | 403 | Your role in the team does not include this permission. | Ask an owner to give you a role that has it. | No |
E02009 | NotATeamMember | 403 | The request names a team you are not in. | Switch to a team you belong to, or ask its owner for an invitation. | No |
E02010 | TeamNotVerified | 403 | Live sending needs a verified team. The team's compliance is pending, has expired or was never sent. | Finish compliance in Settings › Compliance. Until then, use the sandbox. | No |
E02011 | TeamSuspended | 403 | Ponitor suspended the team, so it can't send. | Contact support. | No |
E02012 | WorkspacePaused | 403 | An owner paused the workspace, so it can't send. | Ask an owner of the team to resume it. | No |
E02013 | WorkspaceArchived | 403 | The workspace was archived and does nothing more. | Use another workspace. | No |
E02014 | ProductNotGiven | 403 | The team did not give the workspace SMS, WhatsApp, airtime or USSD. | Ask an owner of the team to give it to the workspace. | No |
E02015 | SenderNotGiven | 403 | The sender belongs to the team, but the workspace may not use it. | Ask an owner of the team to give the sender to the workspace, or use another sender. | No |
E02016 | StaffOnly | 403 | Only Ponitor's staff may do this. | Contact support. | No |
Billing
The balance, a workspace's credit, and prices.
| Code | Name | Status | Meaning | What to do | Retry? |
|---|---|---|---|---|---|
E03001 | InsufficientBalance | 400 | The team's balance can't cover the charge. Nothing was sent or charged. | Add funds under Billing, then send again. | No |
E03002 | InsufficientWorkspaceCredit | 400 | The workspace's credit can't cover the charge. Nothing was sent or charged. | Ask an owner of the team to give the workspace more credit. | No |
E03003 | PricingNotSet | 400 | The team has no price for this channel yet. | Contact support. | No |
E03004 | PriceChanged | 409 | The price is not the expectedPrice you sent. Nothing was charged or sent. Before codes: PRICE_CHANGED. | Read the price again, then send it as expectedPrice. | No |
Sending messages
A message refused when you send it. Nothing is sent or charged.
| Code | Name | Status | Meaning | What to do | Retry? |
|---|---|---|---|---|---|
E10001 | SenderNotFound | 400 | The sender is not one of your team's approved senders for this channel. | Use an approved sender from SMS › Sender IDs, SMS › Short Codes or WhatsApp › Accounts in the app. | No |
E10002 | InvalidRecipient | 400 | The recipient is not a phone number Ponitor can send to. | Send the number in international format, such as +231770123456. | No |
E10003 | NoValidRecipients | 400 | None of the numbers, contacts or lists has a number Ponitor can send to. | Check the numbers, then send again. | No |
E10004 | CountryNotOffered | 400 | SMS to this country is not offered. | See Prices for the countries Ponitor sends to, or contact support to ask for one. | No |
E10005 | TemplateNotFound | 400 | The template id is not an approved template of the sending number. | Use the Template ID from WhatsApp › Templates in the app. | No |
E10006 | OutsideServiceWindow | 400 | The customer hasn't written in the last 24 hours, so WhatsApp takes only a template. | Send an approved template instead. | No |
E10007 | WhatsAppNotConnected | 409 | The WhatsApp Business Account was removed, or the access Ponitor had to it was taken back. Before codes: WHATSAPP_NOT_CONNECTED. | Connect the account again under WhatsApp › Accounts. | No |
E10008 | RecipientOptedOut | 422 | The person unsubscribed from your WhatsApp marketing. Before codes: RECIPIENT_OPTED_OUT. | Don't send them marketing. Utility and authentication templates, and replies, still go. | No |
Delivery
Why a message didn't arrive. These come on the message, as failureCode, in its webhook and from Get a message, never as an error answer.
| Code | Name | failedReason | Meaning | What to do | Send again? |
|---|---|---|---|---|---|
E11001 | RecipientUnreachable | Recipient unreachable: phone was off or out of coverage until the message expired | The phone was off, or out of coverage, for as long as the network kept trying. | Send it again later, when the phone is likely on. | Yes |
E11002 | NumberNotInService | The number may no longer exist | The network says the number is not in service. | Check the number with the person, and stop sending to it if it's wrong. | No |
E11003 | InvalidNumber | Invalid recipient number | The network refused the number as not valid. | Check the number, and send it in international format. | No |
E11004 | CannotReceiveSms | A landline, or a network that can't receive it | The number can't receive SMS: a landline, or a network that takes none. | Use a mobile number. | No |
E11005 | FilteredByNetwork | Filtered by the network | The network's filter held the message back, usually for its content or its sender. | Use a registered Sender ID, and avoid links or words that read as spam. | No |
E11006 | SenderIdRejected | Sender ID not accepted by the network | The network doesn't accept this Sender ID. | Use another Sender ID, or ask support to register this one with the network. | No |
E11007 | NetworkBusy | Network busy, message not accepted | The network was too busy to take the message. It was not charged. | Send it again in a few minutes. | Yes |
E11008 | NetworkUnreachable | The network could not be reached | Ponitor couldn't reach the network, so nothing was sent or charged. | Send it again in a few minutes. | Yes |
E11009 | NotConfirmed | The network did not confirm the message | The network never answered, so the message may or may not have gone out. It was not charged. | Check with the person before you send it again. | No |
E11010 | NetworkRefused | The network did not accept this message | The network refused the message without a reason Ponitor can name. It was not charged. | Check the number and the sender. If both are right and it keeps happening, contact support. | No |
E11011 | Undeliverable | The carrier could not deliver this message | The network accepted the message but could not deliver it, for example to a barred number. | Check the number with the person. | No |
E11012 | RejectedByNetwork | The carrier rejected this message | The network accepted the message, then refused to deliver it. | Check the number and the content. Contact support if it keeps happening. | No |
E11013 | CancelledByNetwork | The carrier cancelled this message | The network cancelled the message before delivering it. | Send it again. | Yes |
E11014 | CountryNotSupported | Ponitor doesn't send SMS to this country yet | SMS to the number's country is not switched on. It was not charged. | Contact support to ask for the country. | No |
E11015 | NotSent | The message could not be sent | Ponitor couldn't send the message. It was not charged. | Send it again. Contact support if it keeps happening. | Yes |
E11016 | NoReasonGiven | The network gave no reason | The network reported the message as not delivered, without saying why. | Send it again later. Contact support if it keeps happening. | Yes |
E11017 | NumberBarred | The number is barred from receiving messages | The network bars the number from receiving messages, for example for an unpaid bill or at its owner's request. | Check the number with the person. Messages will fail until the network lifts the bar. | No |
E11099 | NotDelivered | It wasn't delivered. | The message wasn't delivered, for a reason that isn't one of the codes above. | Contact support with the message id. | No |
E11101 | NotOnWhatsApp | Not a WhatsApp number | The number has no WhatsApp account. | Send an SMS instead. | No |
E11102 | ContentRejected | WhatsApp didn't accept the content | WhatsApp refused the message's content or its format. | Check the content against WhatsApp's rules, then send it again. | No |
E11103 | OutsideReplyWindow | Outside the 24-hour reply window | The customer hadn't written in the last 24 hours, so only a template could go. | Send an approved template instead. | No |
E11104 | SentTooFast | Sent faster than WhatsApp allows | The number sent more than WhatsApp allows in a short time. | Send it again in a few minutes, and spread large sends out. | Yes |
E11105 | MediaUnavailable | A file couldn't be downloaded | WhatsApp couldn't download the image, video, audio or document from its link. | Check that the link is public and works, then send it again. | No |
E11106 | WhatsAppRefusedNumber | Not on WhatsApp, or WhatsApp refused the number | The number isn't on WhatsApp, or WhatsApp won't deliver to it. | Send an SMS instead. | No |
E11107 | TemplateLanguageMissing | The template isn't in that language | The template has no approved version in the language asked for. | Send it in a language the template has, or add that language in WhatsApp › Templates. | No |
E11108 | NumberPausedByWhatsApp | WhatsApp won't send to this number for now | WhatsApp is holding messages to this number back for a while. | Send it again later. | Yes |
E11109 | MarketingHeldBack | WhatsApp held back this marketing message | WhatsApp limits how many marketing messages one person gets, and held this one back. | Send fewer marketing messages to the same people. | No |
E11110 | WhatsAppAccountLocked | Your WhatsApp number or account is locked | WhatsApp locked your number or your WhatsApp Business Account. | Check the account in WhatsApp Manager, then contact support. | No |
E11111 | WhatsAppAccountDisabled | Your WhatsApp Business Account is disabled | WhatsApp disabled your WhatsApp Business Account. | Check the account in WhatsApp Manager, then contact support. | No |
Airtime
A top-up or data bundle refused when you buy it.
| Code | Name | Status | Meaning | What to do | Retry? |
|---|---|---|---|---|---|
E12001 | ProductUnavailable | 400 | The product is not one Ponitor sells now. | List the products again, and use one of them. | No |
E12002 | ProductForOtherNetwork | 400 | The product is sold for numbers on another network than the recipient's. | Use a product for the recipient's network. | No |
E12003 | UnsupportedNetwork | 400 | Airtime goes only to Orange and Lonestar Cell MTN numbers in Liberia. | Use an Orange or MTN number. | No |
E12004 | InvalidMsisdn | 400 | The msisdn is not a phone number. | Send the number in international format, such as 231770123456. | No |
E12005 | TooManyRecipients | 400 | One batch can pay only so many recipients. | Split the recipients into smaller batches. | No |