API v1 overview
Version 1 of the Partners API is a JSON API over HTTPS, called from your server. This page covers the rules every endpoint shares. Each resource then has its own page.
Machine-readable files
Section titled “Machine-readable files”- OpenAPI 3.1 description: import it into your API client or code generator.
- Postman collection: every endpoint, ready to run against the sandbox.
- Postman environment: import it with the collection and fill in your base URL and test key. If you use the environment, set the two values there, not in the collection’s own variables: the environment wins.
Endpoints
Section titled “Endpoints”| Method | Path | What it does |
|---|---|---|
GET |
/v1/countries |
List the countries and regions you can sell. |
GET |
/v1/packages |
List packages, filtered by country or region. |
GET |
/v1/packages/{id} |
Get one package. |
GET |
/v1/balance |
Get your partner balance. |
POST |
/v1/orders |
Buy one or more eSIMs. |
GET |
/v1/orders/{id} |
Get one order. |
GET |
/v1/orders |
List your orders, newest first. |
GET |
/v1/esims |
List your eSIMs. |
GET |
/v1/esims/{iccid} |
Get install details, status and data usage. |
GET |
/v1/esims/{iccid}/topup-packages |
List the packages an eSIM can be topped up with. |
POST |
/v1/esims/{iccid}/topups |
Add a package to an eSIM. |
GET |
/v1/esims/{iccid}/topups |
List the top-ups of an eSIM, newest first. |
GET |
/v1/topups/{id} |
Get one top-up. |
GET |
/v1/events |
List the events of the last 30 days. |
GET |
/v1/events/{id} |
Get one event. |
POST |
/v1/sandbox/esims/{iccid}/simulate |
Test keys only. Move a sandbox eSIM through its lifecycle. |
Base URL
Section titled “Base URL”Your base URL is shown in the Business portal under Developers, on the Overview tab. It ends in /partner/api. The examples in these docs read it from an environment variable:
export ESIMIFY_API_URL="<the base URL from the Developers page>"export ESIMIFY_API_KEY="sk_test_..."Every path starts with /v1. The sandbox and live use the same base URL. The key decides which one you are talking to.
Authentication
Section titled “Authentication”Send your API key as a bearer token on every request:
Authorization: Bearer sk_test_...See Authentication.
Requests and responses
Section titled “Requests and responses”- Bodies are JSON, encoded as UTF-8. Send
Content-Type: application/jsonwith everyPOST. A body with another content type, a body that is not a JSON object, or one larger than 64 KB is refused with400 invalid_request. - Field names are
snake_case. - Fields and query parameters you send that the API does not know are ignored. An empty query value, such as
?status=, counts as absent. The one exception iscursor. - Text you send must not contain control characters, such as a tab or a line break. Lengths are counted in characters. An empty string is treated the same as
null. - A successful response is the object itself, with no wrapper. A list is wrapped as described under Pagination.
- Every response carries an
X-Request-Idheader, such asreq_7c1d92ab34ef5a60. Error bodies repeat it asrequest_id. Log it, and quote it when you contact support. - Responses are sent with
Cache-Control: no-store. The API sends no CORS headers: it is meant to be called from a server, not from a browser.
Pagination
Section titled “Pagination”List endpoints return this shape:
{ "data": [], "has_more": true, "next_cursor": "MTA0ODI"}| Parameter | Type | Description |
|---|---|---|
limit |
integer | How many items to return, from 1 to 100. Default 25. |
cursor |
string | The next_cursor of the previous page. Leave it out for the first page. |
To read everything, keep calling with cursor set to the last next_cursor until has_more is false. On the last page next_cursor is null.
A cursor is opaque. Do not build one yourself and do not store one for long. A cursor only works on the list it came from, with the same kind of key. A limit outside 1 to 100, an empty cursor, or a cursor from another list returns 400 invalid_request.
GET /v1/countries and GET /v1/esims/{iccid}/topup-packages are not paginated. They return data only.
A list is read live, not from a snapshot. If things change while you page through it, an item can be missed or appear twice.
An amount is an object with a decimal string and a currency:
{ "amount": "249.00", "currency": "INR" }amount always has two decimal places. It is a string so that no precision is lost. Parse it with a decimal type, not a float. The currency is INR.
Timestamps are ISO 8601 in UTC, to the second: 2026-10-02T09:14:07Z.
Timestamps you send, such as created_after, can be a date (2026-10-02) or a date and time, with Z or an offset such as +05:30. Without a zone the value is read as UTC. In a query string, write the + of an offset as %2B. A date that does not exist, such as 30 February, returns 400.
Identifiers
Section titled “Identifiers”| Object | Format | Example |
|---|---|---|
| Package | pkg_ and a number |
pkg_10482 |
| Order | ord_ and lower-case letters and digits |
ord_po2610ab12cd |
| Sandbox order | ord_test_ and 12 hex characters |
ord_test_3f9a1c2b7d4e |
| Top-up | top_ and lower-case letters and digits |
top_po2610ef34gh |
| Sandbox top-up | top_test_ and 12 hex characters |
top_test_7a1c9e02b4d6 |
| eSIM | Its ICCID, usually 19 or 20 digits | 8999999000000012345 |
| Event | evt_ and 24 hex characters |
evt_5b81c0d2e4f6a7b8c9d0e1f2 |
| Request | req_ and 16 hex characters |
req_7c1d92ab34ef5a60 |
Sandbox ICCIDs always start with 8999999, so a test eSIM can never be mistaken for a real one.
Treat every identifier as an opaque string. Store it as text, not as a number. Ids are exact and case-sensitive: pkg_010482 and ORD_PO2610AB12CD are not found.
The Business portal shows a live order under its order number, such as PO-2610-AB12CD. The API id is the same number in lower case without the dashes, after ord_.
Safe retries
Section titled “Safe retries”Every POST must carry an Idempotency-Key header. Sending the same key again returns the first result instead of doing the work twice. That is also true when the first result was an order with the status failed: use a new key to try again. See Idempotency.
Rate limits
Section titled “Rate limits”Each key can make 120 requests a minute, of which 30 can be POST. The X-RateLimit-* headers describe the tighter of the two limits for the request. See Rate limits.
Errors every endpoint can return
Section titled “Errors every endpoint can return”Errors use one envelope, described in Errors & rate limits.
| Status | Code | When |
|---|---|---|
400 |
invalid_request |
The request is malformed: for example a body over 64 KB, or text with control characters. |
401 |
invalid_api_key |
The API key is missing, malformed, unknown, revoked or expired. |
403 |
ip_not_allowed |
The request came from an address that is not on the key’s IP allowlist. |
403 |
account_inactive |
Your partner account has been deactivated. Every request is refused. |
403 |
live_mode_not_enabled |
A live key was used, but your account is not approved for live orders yet. |
404 |
not_found |
The path does not exist, or the method is not supported on it. |
429 |
rate_limited |
Too many requests. Wait for the time in Retry-After. Also returned after repeated requests with an invalid key from one address. |
500 |
internal_error |
Something failed on our side. |
Versioning
Section titled “Versioning”The version is part of the path: /v1. While the API is in preview, paths and fields can still change. Each change is listed in the changelog.
After launch, /v1 is meant to change only in backward-compatible ways: new endpoints, new optional parameters, new response fields, new event types and new error codes. A breaking change gets a new version in the path.
Write your client so that it ignores response fields, event types and error codes it does not know. That keeps it working when one is added.

