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

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.

  • 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.
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.

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:

Terminal window
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.

Send your API key as a bearer token on every request:

Authorization: Bearer sk_test_...

See Authentication.

  • Bodies are JSON, encoded as UTF-8. Send Content-Type: application/json with every POST. A body with another content type, a body that is not a JSON object, or one larger than 64 KB is refused with 400 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 is cursor.
  • 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-Id header, such as req_7c1d92ab34ef5a60. Error bodies repeat it as request_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.

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.

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_.

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.

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 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.

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.