Errors & rate limits
The API uses standard HTTP status codes. A 2xx status means the request was accepted. A 4xx status means something in the request needs changing. A 5xx status means the problem is on our side.
A 2xx status does not always mean the purchase worked. An order or a top-up can be created with the status failed: check status in the body before you tell your customer.
Error body
Section titled “Error body”Every error has the same shape:
{ "error": { "code": "insufficient_balance", "message": "Your wallet balance is too low for this request.", "request_id": "req_7c1d92ab34ef5a60" }}| Field | Description |
|---|---|
code |
A stable code. Use it in your program. |
message |
A sentence for a developer. Its wording can change, so do not match on it and do not show it to customers. |
request_id |
The id of the request. It is also in the X-Request-Id response header. Quote it when you contact support. |
param |
The field or header that was wrong, when one is to blame. It is sent with invalid_request for a field, and with the three idempotency errors, where it is Idempotency-Key. |
{ "error": { "code": "invalid_request", "message": "quantity must be an integer between 1 and 50.", "request_id": "req_2a90c4e17b3d5f68", "param": "quantity" }}A path that does not exist, or a method a path does not support, also answers in this shape, with 404 and not_found.
Error codes
Section titled “Error codes”| Status | Code | Meaning | What to do |
|---|---|---|---|
400 |
invalid_request |
A field, a query parameter or the body is missing or not valid. See What makes a request invalid. | Fix the request. param names the field when one field is at fault. Do not retry it unchanged. |
400 |
idempotency_key_required |
A POST was sent without an Idempotency-Key header, or with an empty one. |
Add the header. |
401 |
invalid_api_key |
The key is missing, malformed, unknown, revoked or expired. The message is the same in every case. | Check the key and the Authorization header. |
402 |
insufficient_balance |
Your balance does not cover the order or top-up. Nothing was bought. | Add funds, then send the request again with the same Idempotency-Key. |
403 |
ip_not_allowed |
The request came from an address that is not on the key’s allowlist. | Call from an allowed address, or change the allowlist in the portal. |
403 |
account_inactive |
Your partner account has been deactivated. Every request is refused, with test and live keys. | Email support@esimify.in or ask your eSIMify account manager. |
403 |
live_mode_not_enabled |
A live key was used, but your account is not approved for live orders yet. | Use a test key until your account is approved. |
404 |
not_found |
The path does not exist, the method is not supported on it, or the event does not exist for this key. | Check the path, the method and the id. |
404 |
package_not_found |
No package you can sell has this id. On a top-up: the package is not one of this eSIM’s top-up packages. | Refresh your copy of the catalogue, or list the eSIM’s top-up packages. |
404 |
order_not_found |
No order has this id for your account in this mode. | Check the id and whether you are using the right kind of key. |
404 |
esim_not_found |
No eSIM has this ICCID for your account in this mode. | Check the ICCID and whether you are using the right kind of key. |
404 |
topup_not_found |
No top-up has this id for your account in this mode. | Check the id and whether you are using the right kind of key. |
409 |
idempotency_key_reused |
The same Idempotency-Key was used with a different request body. |
Use a new key for a new request. |
409 |
esim_not_topupable |
The eSIM is expired or cancelled, or cannot take a top-up. | Do not offer a top-up for this eSIM. |
409 |
request_in_progress |
A request with the same Idempotency-Key is still being processed. |
Wait a moment, then send it again with the same key. |
409 |
invalid_state |
Sandbox only. The simulated event does not fit the eSIM’s current status. | Send the events in order: activated first. See Sandbox. |
429 |
rate_limited |
Too many requests, or too many requests with a bad key from your address. | Wait for the time in Retry-After, then retry. |
500 |
internal_error |
Something failed on our side. | Retry with the same Idempotency-Key. |
503 |
supplier_unavailable |
Sandbox only, for the test_supplier_down trigger. The live API does not return it today, but may in future. |
Retry later with the same Idempotency-Key. |
What makes a request invalid
Section titled “What makes a request invalid”400 invalid_request covers:
- a required field that is missing, or a field of the wrong type.
quantitymust be a JSON number, so"2"is refused. - a value outside its range:
quantityoutside 1 to 50,limitoutside 1 to 100, text longer than its limit. - an unknown value for a filter, such as a
statusor an eventtypethat does not exist. - a
cursorthat did not come from the same list with the same kind of key, or an emptycursor. - a timestamp that is not ISO 8601, or a date that does not exist, such as 30 February.
- text with control characters in it, such as a tab or a line break, in
customer_ref,customer.name,customer.email,qorIdempotency-Key. - a body that is not valid JSON, is not a JSON object, or is sent without
Content-Type: application/json. - a body larger than 64 KB. This one is refused before the key is checked.
Lengths are counted in characters, not bytes: one emoji counts as one. Fields you send that the API does not know are ignored.
When a supplier fails in live
Section titled “When a supplier fails in live”The mobile network provider behind a package can fail or be slow. In live you do not get a 503 for that. You get an order or a top-up:
201with an order whosestatusisfailedorpartially_completed. The eSIMs that were not issued are refunded, andrefundedshows the amount.201with an order whosestatusisprocessing. It finishes in the background. See Handlingprocessing.201with a top-up whosestatusisfailed. Nothing was added and the amount was returned.
To try again after a failed result, use a new Idempotency-Key. The old key keeps returning the failed result.
Which errors to retry
Section titled “Which errors to retry”Retry with the same Idempotency-Key |
Do not retry unchanged |
|---|---|
| A timeout or a dropped connection | 400, 401, 403, 404 |
409 request_in_progress |
402 until you have added funds |
429, after the Retry-After time |
409 idempotency_key_reused |
500 and 503 |
409 esim_not_topupable and 409 invalid_state |
Wait a little longer between each attempt, and stop after a few.
Idempotency
Section titled “Idempotency”A request is idempotent when sending it twice has the same effect as sending it once. This is what stops a timeout from turning into two purchases.
The rules:
- Every
POSTmust carry anIdempotency-Keyheader. Without it the API answers400withidempotency_key_required. - The key is 1 to 255 characters, with no control characters. A longer key gets
400 invalid_request. Use a value that identifies the action on your side, such as your booking number. - Same key, same body: you get the first response again, with status
200and the headerIdempotent-Replayed: true. Nothing is done twice. - Same key, different body: the API answers
409withidempotency_key_reused. Nothing is done. - Same key while the first request is still running: the API answers
409withrequest_in_progress. Wait and send it again. - Keys are separate for each partner account and each mode. A key used in the sandbox does not clash with the same key in live. All API keys of your account in one mode share the same idempotency keys, so a retry with a rolled key is safe.
- A key belongs to one request on one endpoint. Using an order’s key for a top-up, or for a different eSIM, counts as a different request and gets
409 idempotency_key_reused. - A key is remembered for at least 24 hours. After it has been forgotten, the same key creates a new order. For a late retry, look the order up first with
GET /v1/orders?customer_ref=....
What is remembered and what is not:
- Only accepted requests are remembered, meaning responses with a
2xxstatus. An order or top-up with the statusfailedis one of them: sending the same key again returns that failed result and buys nothing. To try the purchase again, use a new key. - Errors are not remembered. After
400,402,404,409,500or503, the same key runs the request again. That is why a402can be retried with the same key once you have added funds. - A replay is the first response, exactly as it was sent, not the object’s current state. The one exception is a live order or top-up that was first answered as
processing: a retry with the same key returns it as it is now. In the sandbox the firstprocessinganswer is replayed, so fetch the order withGET /v1/orders/{id}.
curl -X POST "$ESIMIFY_API_URL/v1/orders" \ -H "Authorization: Bearer $ESIMIFY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: booking-84512" \ -d '{ "package_id": "pkg_10482", "quantity": 1, "customer_ref": "booking-84512" }'If that request times out, you cannot know whether it reached us. Send exactly the same request again, with the same key. You get the result of the first one.
Choosing a key:
- One key for each thing you want to happen once. One booking that buys one order gets one key.
- If a booking can lead to more than one purchase, add a suffix:
booking-84512-topup-1. - Do not generate a new random key on each retry. That defeats the purpose.
- Do not start a key with
api:. That prefix is reserved.
GET requests need no key. They are always safe to repeat.
Rate limits
Section titled “Rate limits”Limits are counted for each API key, in fixed windows of one minute.
| Limit | Requests per minute |
|---|---|
| All requests | 120 |
POST requests |
30 |
A POST counts towards both limits. A POST that is refused by the POST limit does not use up the general one.
Every response to a request with a valid key tells you where you stand, including 400, 403 and 404 responses:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The limit that applies to this request: the tighter of the two. It shows 30 on a POST and 120 on a GET. |
X-RateLimit-Remaining |
How many requests you have left in the current window. |
X-RateLimit-Reset |
When the window resets, in seconds since the Unix epoch. |
Retry-After |
Sent with 429 only. How many seconds to wait before you try again. |
A 401 response does not carry the X-RateLimit-* headers, because no key was recognised. Nor does the 429 for too many failed attempts, described below.
When you pass a limit, the API answers 429 with the code rate_limited. Wait for the time in Retry-After, then carry on. Because the window is fixed, the wait can be up to a full minute.
To stay well under the limits:
- Cache the catalogue. It changes slowly, so do not fetch it on every page view.
- Use webhooks instead of polling orders and eSIMs.
- Spread bulk work out instead of sending it in one burst.
Other guards:
- Failed authentication. After 60 requests with a bad key from one IP address within 10 minutes, every request from that address gets
429 rate_limitedwithRetry-Afteruntil the 10 minutes are over, including requests with a valid key. Everything behind one NAT address shares this count. - The QR code image.
qr_code_urlallows 240 requests a minute from one IP address.
Headers on every response
Section titled “Headers on every response”| Header | Value |
|---|---|
X-Request-Id |
The id of the request, such as req_7c1d92ab34ef5a60. On every response, errors included. |
Cache-Control |
no-store. Responses are private to you and must not be cached by a proxy. |
X-RateLimit-* |
As described above. |
Idempotent-Replayed |
true on a replayed POST. Absent otherwise. |

