API v1 · Stable

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.

Instant provisioning

QR + activation code returned in seconds via REST.

Prepaid wallet

Top up once, then every purchase is one API call.

One price, one plan

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

  1. Create an API key in the reseller panel.
  2. Top up your wallet (min $50) with the Top up button.
  3. List the catalog to find a product_id.
  4. 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

FieldTypeDescription
product_idstring (uuid)Pass to /purchase.
namestringHuman-readable plan name.
regionstringRegion label or comma-separated country codes.
countriesstring[]ISO alpha-2 codes covered.
data_amount_mbintegerBundle size in MB. Null when unlimited.
data_unlimitedbooleanTrue for uncapped plans.
validity_daysintegerDays from first activation.
networksobject[]Local carriers used, when known.
price_usdnumberYour 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

FieldTypeDescription
product_idstring (uuid), requiredFrom /catalog.
quantityinteger 1–10, default 1Number of eSIMs to order.
customer_emailstring, optionalEnd-customer email (recorded on the order).
referencestring ≤128, optionalYour internal reference — echoed back in order metadata.

Headers

FieldTypeDescription
AuthorizationBearer sk_live_…, requiredAPI key.
Idempotency-Keystring ≤128, recommendedReplaying the same key returns the original response and never re-debits.

Response fields

FieldTypeDescription
orders[]object[]One entry per unit ordered.
orders[].order_numberstringUse with /orders/:order_number for polling.
orders[].statusstringpaid | fulfilled | failed.
orders[].esimobject | nullQR + activation fields when ready, otherwise null.
total_usdnumberDebited from your wallet.
balance_usdnumberWallet 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": "…" } }
FieldTypeDescription
400 invalid_requestobjectBody failed validation. See message + issues[].
401 unauthorizedobjectMissing/invalid/revoked API key.
402 insufficient_fundsobjectWallet too low. Body includes balance_usd + required_usd.
403 insufficient_scopeobjectKey is missing the required scope (read or write).
404 not_found / product_not_foundobjectResource does not exist.
409 replayobjectNot thrown — replays return 200 with the original result. See Idempotency.
429 rate_limitedobjectSlow down. Body includes retry_after_seconds.
5xxobjectTransient 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.

Ready to build?

Grab a key and top up in one place.

Open the API panel