eSIM API for your business
Plug global travel eSIMs into your product with a single REST call. Prepay a wallet, hit POST /purchase, get a QR code back. No merchant onboarding, no per-order checkout, no telecom deals.
QR + activation code returned in seconds via REST.
Top up once, then every purchase is one API call.
USD-only. No FX, no surge, no tiers.
Overview
The esimreseller.io API is a wallet-funded, REST/JSON eSIM procurement service. You top up a USD balance from your reseller panel; every POST /purchase call debits it and provisions an eSIM through our upstream carrier. Prices are USD-only and shown as a single number per plan — what you see is what you pay.
Base URL: https://esimreseller.io/api/v1
Content type: application/json
Currency: USD.
Authentication
Every request needs an Authorization: Bearer sk_live_… header. Create keys in your reseller panel under API & Wallet. Keys carry read and/or write scopes (default: both). Rotate any key with the Revoke button in the panel and mint a new one — no downtime.
Authorization: Bearer sk_live_your_key_here
Rate limits. 60 write requests / minute per key (purchase); 120 requests / minute per key on read endpoints. When exceeded, the API returns 429 with a retry_after_seconds field.
Key safety. Never commit keys to source control. Never expose them in a browser bundle — keys carry full purchase authority for your wallet.
Quickstart
- Create an API key in the reseller panel.
- Top up your wallet (min $50) with the Top up button.
- List the catalog to find a
product_id. - POST a purchase, get a QR back.
1. List the catalog
curl https://esimreseller.io/api/v1/catalog \ -H "Authorization: Bearer sk_live_…"
// JavaScript
const r = await fetch("https://esimreseller.io/api/v1/catalog", {
headers: { Authorization: "Bearer sk_live_…" },
});
const { items } = await r.json();2. Buy an eSIM
curl -X POST https://esimreseller.io/api/v1/purchase \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"product_id": "b3c…-uuid",
"quantity": 1,
"customer_email": "buyer@example.com",
"reference": "order-42"
}'Response (201 Created):
{
"orders": [{
"order_number": "ES-8F2A1C",
"status": "fulfilled",
"esim": {
"qr_code_url": "https://…/qr.png",
"activation_code": "LPA:1$smdp.example$ABC123…",
"smdp_address": "smdp.example",
"iccid": "89012345678901234567"
}
}],
"total_usd": 8.50,
"balance_usd": 91.50
}GET /catalog
List every plan available to your account, priced per unit.
Query params: country (ISO 3166 alpha-2, e.g. SA) — filter to plans covering that country.
Response fields
| Field | Type | Description |
|---|---|---|
| product_id | string (uuid) | Pass to /purchase. |
| name | string | Human-readable plan name. |
| region | string | Region label or comma-separated country codes. |
| countries | string[] | ISO alpha-2 codes covered. |
| data_amount_mb | integer | Bundle size in MB. Null when unlimited. |
| data_unlimited | boolean | True for uncapped plans. |
| validity_days | integer | Days from first activation. |
| networks | object[] | Local carriers used, when known. |
| price_usd | number | Your unit price. All-in. |
POST /purchase
Debits your wallet and provisions quantity eSIMs. Returns as soon as the upstream carrier confirms; if a QR isn't ready in the initial response, poll GET /orders/:order_number.
Request body
| Field | Type | Description |
|---|---|---|
| product_id | string (uuid), required | From /catalog. |
| quantity | integer 1–10, default 1 | Number of eSIMs to order. |
| customer_email | string, optional | End-customer email (recorded on the order). |
| reference | string ≤128, optional | Your internal reference — echoed back in order metadata. |
Headers
| Field | Type | Description |
|---|---|---|
| Authorization | Bearer sk_live_…, required | API key. |
| Idempotency-Key | string ≤128, recommended | Replaying the same key returns the original response and never re-debits. |
Response fields
| Field | Type | Description |
|---|---|---|
| orders[] | object[] | One entry per unit ordered. |
| orders[].order_number | string | Use with /orders/:order_number for polling. |
| orders[].status | string | paid | fulfilled | failed. |
| orders[].esim | object | null | QR + activation fields when ready, otherwise null. |
| total_usd | number | Debited from your wallet. |
| balance_usd | number | Wallet balance after this call. |
Idempotent retry example
Network dropped mid-response? Replay with the same key — you get the original result, no second charge.
# 1st call — succeeds server-side but response lost curl -X POST https://esimreseller.io/api/v1/purchase -H "Idempotency-Key: 6f8e…" … # 2nd call, same key — same 200 response, no new order/debit curl -X POST https://esimreseller.io/api/v1/purchase -H "Idempotency-Key: 6f8e…" …
GET /orders/:order_number
Poll for QR/activation details.
curl https://esimreseller.io/api/v1/orders/ES-8F2A1C \ -H "Authorization: Bearer sk_live_…"
{
"order": {
"order_number": "ES-8F2A1C",
"status": "fulfilled",
"esim": {
"qr_code_url": "https://…/qr.png",
"activation_code": "LPA:1$smdp.example$ABC…",
"smdp_address": "smdp.example",
"iccid": "89012…"
},
"product": { "name": "Japan 5 GB / 30d" },
"created_at": "2026-07-01T12:00:00Z"
}
}GET /wallet
{ "balance_usd": 91.50, "currency": "USD", "low_balance_threshold_usd": 25 }GET /wallet/transactions
Paged ledger, newest first. Query params: limit (1–200, default 50), before (ISO timestamp for pagination).
{
"items": [
{ "type": "purchase", "amount_usd": -8.50, "balance_after_usd": 91.50,
"order_number": "ES-8F2A1C", "note": null,
"created_at": "2026-07-01T12:00:01Z" },
{ "type": "topup", "amount_usd": 100, "balance_after_usd": 100,
"order_number": null, "note": "Stripe top-up",
"created_at": "2026-07-01T11:58:00Z" }
],
"has_more": false,
"next_before": null
}Errors
All errors follow the same JSON shape:
{ "error": { "code": "…", "message": "…" } }| Field | Type | Description |
|---|---|---|
| 400 invalid_request | object | Body failed validation. See message + issues[]. |
| 401 unauthorized | object | Missing/invalid/revoked API key. |
| 402 insufficient_funds | object | Wallet too low. Body includes balance_usd + required_usd. |
| 403 insufficient_scope | object | Key is missing the required scope (read or write). |
| 404 not_found / product_not_found | object | Resource does not exist. |
| 409 replay | object | Not thrown — replays return 200 with the original result. See Idempotency. |
| 429 rate_limited | object | Slow down. Body includes retry_after_seconds. |
| 5xx | object | Transient upstream failure — retry with backoff. |
402 insufficient_funds example
{
"error": {
"code": "insufficient_funds",
"message": "Your wallet balance is too low for this purchase. Top up in your reseller panel.",
"balance_usd": 3.20,
"required_usd": 8.50
}
}Polling for eSIM details
Most purchases return the QR in the initial POST /purchase response. When they don't, theesim field is null and status is paid — pollGET /orders/:order_number with backoff until esim is populated (usually within a few seconds).
async function waitForEsim(orderNumber) {
for (const wait of [500, 1000, 2000, 4000, 8000, 15000]) {
await new Promise(r => setTimeout(r, wait));
const r = await fetch("https://esimreseller.io/api/v1/orders/" + orderNumber, {
headers: { Authorization: "Bearer sk_live_…" }
});
const { order } = await r.json();
if (order.esim?.qr_code_url) return order.esim;
}
throw new Error("eSIM not ready — contact support");
}Changelog
- v1.0 — Initial release: catalog, purchase, orders, wallet, transactions.