Skip to content
Preview. The Partners API is not live yet, so details on this page can change before launch.

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.

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.

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.

400 invalid_request covers:

  • a required field that is missing, or a field of the wrong type. quantity must be a JSON number, so "2" is refused.
  • a value outside its range: quantity outside 1 to 50, limit outside 1 to 100, text longer than its limit.
  • an unknown value for a filter, such as a status or an event type that does not exist.
  • a cursor that did not come from the same list with the same kind of key, or an empty cursor.
  • 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, q or Idempotency-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.

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:

  • 201 with an order whose status is failed or partially_completed. The eSIMs that were not issued are refunded, and refunded shows the amount.
  • 201 with an order whose status is processing. It finishes in the background. See Handling processing.
  • 201 with a top-up whose status is failed. 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.

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.

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 POST must carry an Idempotency-Key header. Without it the API answers 400 with idempotency_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 200 and the header Idempotent-Replayed: true. Nothing is done twice.
  • Same key, different body: the API answers 409 with idempotency_key_reused. Nothing is done.
  • Same key while the first request is still running: the API answers 409 with request_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 2xx status. An order or top-up with the status failed is 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, 500 or 503, the same key runs the request again. That is why a 402 can 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 first processing answer is replayed, so fetch the order with GET /v1/orders/{id}.
Terminal window
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.

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_limited with Retry-After until 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_url allows 240 requests a minute from one IP address.
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.