Download OpenAPI specification:
Buy eSIMs for your travellers, top them up, refund them, and get notified when they run low on data.
Base URL: https://api.esimdatastore.com/api
| Topic | Details |
|---|---|
| Authentication | Send your API key in the X-API-Key header on every request. A missing or invalid key returns 401. |
| Sandbox and live | Keys starting with ek_sandbox_ work in the test environment (see Sandbox); ek_live_ keys buy real eSIMs with real money. Same URL and endpoints for both. |
| Wallet | Your account is prepaid. Orders and top-ups are paid from your wallet and refunds go back to it. Check it with GET /wallets/balance; contact us to add funds. |
| Format | JSON in and out. Money amounts are decimal strings, e.g. "4.50". |
Keep your key on your server, never in an app or web page. If it may have leaked, ask us to rotate it; the old key stops working immediately.
1. Pick a plan. GET /plans returns your catalogue with your prices. price is what a new eSIM costs: planPrice plus your activationFee. The activation fee is charged once per new eSIM and is not refundable; top-ups carry no fee.
curl https://api.esimdatastore.com/api/plans -H "X-API-Key: $API_KEY"
2. Place the order. The plan's price is taken from your wallet. Send a new Idempotency-Key with each order (see Retries and idempotency).
curl -X POST https://api.esimdatastore.com/api/orders \
-H "X-API-Key: $API_KEY" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{"planId": "<plan id>"}'
3. Wait for it to complete. If the order is not yet COMPLETED, poll GET /orders/{id} every few seconds. PENDING and CONFIRM_PENDING mean it is still being set up; FAILED and CONFIRM_FAILED mean it could not be fulfilled and you are not charged.
4. Get the install code. Find the order's iccid in GET /esims, then call GET /esims/{id}. Its esimQr (LPA:1$…) is what the traveller scans as a QR code or enters manually.
Usage. GET /esims/{id}/usage returns data used, activation time and expiry. To react to low data without polling, use webhooks.
Activation. POST /esims/iccid/{iccid}/activate starts a pending eSIM's plan right away; liveStatus.status on GET /esims/{id} then shows the network's state, normally ACTIVE. Calling it again on an active eSIM returns 409. Once an eSIM is active, its order can no longer be refunded.
Top up. GET /esims/{id}/topups/available lists the plans you can add to an eSIM and their price: the plan's planPrice, with no activation fee, since no new eSIM is issued. POST /esims/{id}/topup with { "planId": "…" } creates a top-up order; follow it like a purchase.
Refund. An order can be refunded while it is COMPLETED, its eSIM has not been activated, and it is no more than 60 days old. POST /orders/{id}/refund returns 202; poll GET /orders/{id} until it is REFUNDED (or REFUND_FAILED — contact us).
| Order | Credited back to your wallet (refundedAmount) |
|---|---|
| New eSIM | Price paid minus the activation fee (shown in nonRefundableFee) |
| Top-up | Full price paid |
If the eSIM has top-ups that are still charged, refund those first; until then the refund returns 409 listing the top-up order IDs. Refunding a new-eSIM order blocks the eSIM: it can no longer be installed or topped up. fullRefund: true means our support team refunded the order in full, activation fee included.
Send an Idempotency-Key header (a UUID you generate) on POST /orders, POST /orders/{id}/refund, POST /esims/{id}/topup, PUT /webhooks/endpoint and POST /webhooks/endpoint/rotate-secret. Use a new key for each operation, and the same key when retrying it after a timeout or 5xx — a retry can then never charge you twice.
Within 24 hours, repeating a key returns the original response if the first request succeeded, 409 if it is still running, and runs the request again if it failed. Reusing a key with a different request body returns 422 (code: "IDEMPOTENCY_KEY_REUSED") and nothing runs, so always use a new key for a new operation.
Errors use application/problem+json:
{
"type": "https://esimdatastore.com/errors/conflict",
"title": "Conflict",
"status": 409,
"detail": "Insufficient balance",
"code": "CONFLICT"
}
Branch on the HTTP status; detail explains the reason.
| Status | Meaning |
|---|---|
401 |
Missing or invalid API key. |
404 |
Not found, or it belongs to another account. |
409 |
Not possible right now, e.g. insufficient balance, order not refundable, or another request for the same eSIM in progress. |
422 |
Invalid request, e.g. a bad field, unknown plan ID, or an Idempotency-Key reused with a different body. Field errors are listed in errors. |
429 |
Rate limited. Wait Retry-After seconds. |
502, 503 |
Network provider or service temporarily unavailable. Retry with the same Idempotency-Key, after Retry-After if present. |
500 |
Unexpected error. Retry later; contact us if it persists. |
Limits are counted per client IP address, per minute: 30 for reading plans, orders and the wallet balance, and listing eSIMs; 10 for placing orders, top-ups, activation and any call on a single eSIM; 5 for refunds. GET /esims/{id}/usage can also be called only once per minute per eSIM. Over a limit you get 429; wait the number of seconds in the Retry-After header.
We can notify your server when an eSIM is running out of data, so you can offer a top-up.
Set up. PUT /webhooks/endpoint with { "url": "https://…" } registers your public HTTPS endpoint. We immediately send it a signed webhook.test event, which must get a 2xx within 10 seconds. The response contains your signing secret — store it securely. Re-registering with a new URL keeps the same secret; POST /webhooks/endpoint/rotate-secret replaces it. GET shows the endpoint's status (never the secret) and DELETE stops deliveries.
| Event | Sent when |
|---|---|
esim.low_data |
An eSIM has used 80% or more of its data. Once per eSIM. |
esim.depleted |
An eSIM has used all of its data. Once per eSIM. If usage jumps straight to 100%, only this event is sent. |
webhook.test |
You register an endpoint or rotate the secret. |
{
"id": "0199f2c4-8a1e-7b3c-9d40-6e5f1a2b3c4d",
"type": "esim.low_data",
"created_at": "2026-10-20T09:26:40.000Z",
"data": {
"esim_id": "…",
"iccid": "8944…",
"order_id": "…",
"plan": { "id": "…", "name": "Europe 10 GB / 30 days" },
"usage_bytes": 8589934592,
"usage_limit_bytes": 10737418240,
"usage_percent": 80,
"service_activated_at": "2026-10-12T07:03:11.000Z",
"service_expires_at": "2026-11-11T07:03:11.000Z"
}
}
Ignore fields you don't recognise; we may add new ones.
Verify the signature. Each delivery has X-Esimds-Signature: t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256(secret, "<t>.<raw body>"). Check it against the body exactly as received, before parsing JSON, and reject timestamps more than 5 minutes old. In Node.js:
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const fields = {};
for (const part of header.split(',')) {
const [key, value, ...rest] = part.split('=');
if (rest.length || !['t', 'v1'].includes(key) || key in fields) return false;
fields[key] = value;
}
if (!/^\d+$/.test(fields.t ?? '') || !/^[0-9a-f]{64}$/i.test(fields.v1 ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(fields.t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${Number(fields.t)}.${rawBody}`).digest();
return crypto.timingSafeEqual(Buffer.from(fields.v1, 'hex'), expected);
}
Delivery. Answer 2xx within 10 seconds and do slow work afterwards. On a network error, timeout, 408, 429 or 5xx we retry after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h; other 4xx responses are not retried. An event can arrive more than once — use its id to skip duplicates. After 20 failures in a row we pause for an hour; after 100 we disable the endpoint until you call PUT /webhooks/endpoint again.
The sandbox lets you build and test without real eSIMs or real money. It works exactly like live — same URL, endpoints, responses, errors, rate limits, idempotency, wallet charges and refund rules, and webhook signatures — except for the differences below. Sandbox and live are separate accounts; we load your sandbox price list and test funds, and top them up on request.
| In the sandbox | |
|---|---|
| eSIMs | Not real and cannot be installed. ICCIDs start with 89990 and the activation code points to sandbox.example.com. |
| Plans | Any plan in your sandbox price list can be ordered; every eSIM behaves like a 10 GB, 30-day plan. |
| Orders and top-ups | Returned as CONFIRM_PENDING, as in live, and complete shortly after. Poll GET /orders/{id} the same way. |
| Refunds | Usually return REFUNDED straight away. |
| eSIM status and usage | liveStatus.status is PROVISIONED with 0 used until you call activate; then ACTIVE with 1 GB used of 10 GB, which never changes. |
| Available top-ups | Every plan in your price list (in live, only plans compatible with that eSIM). |
| Low-data webhooks | Never sent, as sandbox eSIMs use no data. Test them in live with a small plan on a test phone. |
| Older test orders | Details can change after we restart the service; use fresh orders for each test run. |
Testing failures. Use these plan IDs with POST /orders or POST /esims/{id}/topup. You are not charged.
| Plan ID | Result |
|---|---|
sandbox-error-timeout |
502 — the network provider timed out. |
sandbox-error-provision-fail |
502 — the network provider failed to create the eSIM. |
sandbox-error-rate-limit |
502 — the network provider is rate-limiting. |
any other sandbox-error-… |
422 — unknown plan. |
Going live. Before switching, make sure you can buy, deliver and refund an eSIM end to end, retry with the same Idempotency-Key without a double charge, verify the webhook.test signature, and handle 409, 429 and 502. Then tell us: we create your live account and send an ek_live_ key. Swap the key — nothing else changes. Your live wallet is separate from your sandbox wallet.
GET /esims lists your eSIMs; GET /esims/:id returns one, including the esimQr activation code the traveller installs and its current network status. GET /esims/:id/usage returns data used, activation time and expiry; it can be called once per minute per eSIM, and sooner returns 429 with Retry-After.
GET /esims/:id/topups/available lists the plans you can add to an eSIM. A top-up costs the plan's planPrice; the activation fee is charged only on a new eSIM. POST /esims/:id/topup applies one and charges your wallet. A BLOCKED eSIM (its order was refunded) has no top-ups, and POST /esims/:id/topup returns 409 without charging. status is the eSIM's lifecycle (e.g. PROVISIONED, BLOCKED); liveStatus.status is its network state as reported by the provider, normally ACTIVE once activated. In the sandbox, liveStatus.status is PROVISIONED with no usage until activated, then ACTIVE with fixed usage.
Activates a PENDING subscription for the given ICCID. Sandbox activations return a simulated ACTIVE status.
| iccid required | string |
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}liveStatus.status is the network state: PROVISIONED until activated, then ACTIVE (sandbox included).
| id required | string |
{- "id": "string",
- "iccid": "string",
- "esimQr": "string",
- "status": "PROVISIONED",
- "createdAt": "2019-08-24T14:15:22Z",
- "liveStatus": {
- "iccid": "string",
- "esimQr": "string",
- "status": "string"
}
}Returns live usage from the network, refreshed at most once per 60 seconds per eSIM. Calls within that window receive 429 with a Retry-After header; cache the response for the interval. Sandbox returns fixed usage.
| id required | string |
{- "iccid": "string",
- "usedAmount": 0,
- "totalAmount": 0,
- "amountUnit": "string",
- "status": "string",
- "activationTime": "string",
- "expiry": "string"
}Sandbox returns every plan in your price list. A BLOCKED eSIM returns an empty list.
| id required | string |
[- {
- "planId": "string",
- "name": "string",
- "duration": 0,
- "durationUnit": "string",
- "amount": 0,
- "amountUnit": "string",
- "coverage": [
- "string"
], - "price": "string",
- "planPrice": "14.50",
- "activationFee": "0.00"
}
]Sandbox top-ups complete immediately without a real eSIM.
| id required | string |
| planId required | string ID of the top-up plan to apply. Sandbox only: |
{- "planId": "string"
}{- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "15.00",
- "planPrice": "14.50",
- "activationFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50",
- "nonRefundableFee": "0.50",
- "fullRefund": false
}Purchase eSIM plans and follow their status. POST /orders charges your prepaid wallet and returns the order, which moves through PENDING / CONFIRM_PENDING to COMPLETED, or to FAILED / CONFIRM_FAILED (not charged). Include an Idempotency-Key header on every order; repeating a key within 24 hours returns the original response instead of charging twice.
Fetch a single order with GET /orders/:id, or page through all orders with GET /orders?limit=&cursor= (max 100 per page). Sandbox orders create test eSIMs (ICCID prefix 89990) that cannot be installed.
Sandbox customers receive dummy eSIMs with no real provisioning.
| planId required | string ID of the plan to purchase. Sandbox only: |
{- "planId": "string"
}{- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "15.00",
- "planPrice": "14.50",
- "activationFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50",
- "nonRefundableFee": "0.50",
- "fullRefund": false
}[- {
- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "15.00",
- "planPrice": "14.50",
- "activationFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50",
- "nonRefundableFee": "0.50",
- "fullRefund": false
}
]{- "id": "string",
- "planId": "string",
- "iccid": "string",
- "operationType": "NEW",
- "status": "PENDING",
- "subscriptionStatus": "PENDING",
- "salePrice": "15.00",
- "planPrice": "14.50",
- "activationFee": "0.50",
- "createdAt": "2019-08-24T14:15:22Z",
- "refundRequestedAt": "string",
- "refundedAt": "string",
- "refundedAmount": "14.50",
- "nonRefundableFee": "0.50",
- "fullRefund": false
}GET /plans returns the plans available to your account at your prices. Each plan carries its name, coverage, duration, data allowance and price, the full amount charged for a new eSIM: planPrice plus activationFee. The activation fee is charged once per new eSIM and is not refundable; a top-up costs planPrice only. Use its id as the planId for POST /orders and POST /esims/:id/topup.
[- {
- "id": "string",
- "name": "string",
- "description": "string",
- "price": "15.00",
- "planPrice": "14.50",
- "activationFee": "0.50",
- "duration": 30,
- "durationUnit": "DAY",
- "dataAmount": 10,
- "dataAmountUnit": "GB",
- "coverage": "US"
}
]Refunds are asynchronous. POST /orders/:id/refund returns 202 Accepted; poll GET /orders/:id until status is REFUNDED or REFUND_FAILED. An order can be refunded while it is COMPLETED, its eSIM has not been activated, and it is no more than 60 days old.
refundedAmount is what goes back to your wallet: the price paid minus the non-refundable activation fee for a new eSIM (shown in nonRefundableFee), or the full price for a top-up. Sandbox refunds complete immediately.
Refunding a new-eSIM order blocks its eSIM: GET /esims/:id shows status: "BLOCKED" and top-ups return 409. A top-up refund leaves the eSIM unchanged. Retrying with the same Idempotency-Key returns the original 202; a fresh key on a refunded order returns 409. While a top-up or another refund for the same eSIM is in progress, the refund returns 409; retry shortly. A new-eSIM refund also returns 409, listing the top-up order IDs, while any top-up on that eSIM is still charged: refund those first.
Accepts the refund and returns 202. The body reports REFUNDED if the refund completes within a few seconds; otherwise REFUND_PENDING. Poll GET /orders/:id until status becomes REFUNDED or REFUND_FAILED. Sandbox refunds complete immediately.
| id required | string |
{- "orderId": "string",
- "status": "REFUND_PENDING",
- "refundRequestedAt": "2026-05-13T12:00:00.000Z",
- "refundedAt": "2026-05-13T12:00:01.234Z",
- "refundedAmount": "14.50",
- "nonRefundableFee": "0.50",
- "fullRefund": false
}Your prepaid wallet funds every purchase. GET /wallets/balance returns the current balance as a decimal string (e.g. "150.00"). Orders and top-ups are paid from the wallet when they are placed; refunds are credited back once they complete. To add funds, contact us.
Register one HTTPS endpoint to receive esim.low_data and esim.depleted events. Payload format, signature verification and retry behaviour are described in the Webhooks guide.
{- "enabled": true,
- "verifiedAt": "2026-08-31T14:22:31.000Z",
- "lastDeliveryAt": "2026-08-31T15:07:12.000Z",
- "consecutiveFailures": 0
}Generates a fresh signing secret, POSTs a signed webhook.test event to the supplied URL synchronously, and persists only on 2xx. The plaintext secret is returned exactly once — store it out-of-band. A non-2xx response, network error, or a URL that is not public HTTPS returns 422 and nothing is saved.
| url required | string HTTPS URL that will receive signed webhook deliveries. Must be publicly reachable — private/loopback/link-local addresses are rejected. |
{
}{- "secret": "whsec_dGVzdC1zZWNyZXQtdmFsdWUtZm9yLWRvY3M",
- "verifiedAt": "2026-08-31T14:22:31.000Z"
}Clears the registered URL, secret hash, and cool-off state. Idempotent — safe to call when no endpoint is registered.
{- "title": "Gone",
- "status": 410,
- "detail": "install token expired",
- "code": "GONE",
- "errors": [
- { }
], - "retryAfterSeconds": 30
}Re-runs the handshake against the existing URL with a fresh secret and swaps the stored hash only on 2xx. The plaintext new secret is returned exactly once. The old secret remains valid until this call commits — in-flight deliveries signed under it verify normally on the caller side. 404 if no endpoint is registered or the endpoint has been auto-disabled.
{- "secret": "whsec_dGVzdC1zZWNyZXQtdmFsdWUtZm9yLWRvY3M",
- "verifiedAt": "2026-08-31T14:22:31.000Z"
}