The eSIMCenter Partner API gives you our full catalog — 200+ destinations, plans from several providers — and lets you buy an eSIM from your prepaid wallet with a single POST. The QR code comes back in the same response.
Test key, fake eSIMs, no charges. Start here. Live key: real orders, charged to your wallet.
https://sandbox-api.esimcenter.com/v3selectedhttps://api.esimcenter.com/v3selectedX-API-Key: esk_test_YOUR_SANDBOX_KEYJSON over HTTPS, UTF-8 · 360–900 requests / minute per key · the same endpoints on both addresses.
Get an API keySign in and create your own key from the developer panel — no waiting on us. Want a scheduled-expiry key instead of the default no-expiry one? ask us.
Three steps from nothing to a live integration. Start in the sandbox: the same API on its own address, with a test key, fake eSIMs and no charges.
Sign in to the developer panel and pick Sandbox in the key form. A sandbox key starts with esk_test_. The full key is shown once; we keep only a hash, so a lost key cannot be recovered, only replaced.
Export the address and key once. Every shell sample on this page uses these two variables.
export BASE="https://sandbox-api.esimcenter.com/v3"
export KEY="esk_test_YOUR_SANDBOX_KEY"List the plans for one country. A 200 with "result": "ok" means your key and address are right.
curl -s "https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr" \
-H "X-API-Key: esk_test_YOUR_SANDBOX_KEY" \
-H "Accept: application/json"<?php
$ch = curl_init("https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: esk_test_YOUR_SANDBOX_KEY",
"Accept: application/json",
],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($body, true);
if ($status !== 200 || ($data["result"] ?? "") !== "ok") {
// Branch on $data["error"] - it is a stable machine code, never a sentence.
throw new RuntimeException("API error: " . ($data["error"] ?? "unknown"));
}
foreach ($data["products"] as $p) {
printf("%-34s %6s %3sd $%s\n", $p["id"], $p["data"], $p["day"], $p["price"]);
}const res = await fetch("https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr", {
headers: { "X-API-Key": "esk_test_YOUR_SANDBOX_KEY", Accept: "application/json" },
});
const data = await res.json();
if (!res.ok || data.result !== "ok") {
// data.error is a machine code: "invalid_api_key", "rate_limited", ...
throw new Error(`API error: ${data.error}`);
}
for (const p of data.products) {
console.log(p.id, p.data, `${p.day}d`, `$${p.price}`);
}Then buy a test eSIM. Take any id from the catalog answer. The order is a sandbox order: your balance stays at 1000.00 and the eSIM it returns is fake. Pick the outcome with sandbox_scenario and the usage figures with sandbox_usage — see Sandbox.
curl -s -X POST "$BASE/purchase/buy" \
-H "X-API-Key: $KEY" \
-d "product_id=PRODUCT_ID_FROM_THE_CATALOG" \
-d "idempotency_key=$(uuidgen)"When the sandbox run is clean, switch both values. Nothing else in your code changes:
export BASE="https://api.esimcenter.com/v3"
export KEY="esk_YOUR_LIVE_KEY"Create the live key in the same developer panel. A mixed-up pair fails loudly instead of spending money — see Environments. Ready-made Postman and OpenAPI files are under Downloads.
Two addresses, same endpoints and field names. What makes a request a sandbox request is the key, never a field in the request: no body or query value can turn a sandbox key into a live one or the other way round. Click an address to copy it.
| Environment | Base URL | Key | What happens |
|---|---|---|---|
| Sandbox | https://sandbox-api.esimcenter.com/v3 | esk_test_ + 40 hex characters | No money moves, no provider is called, the eSIM is fake. Every response carries "livemode": false. |
| Live | https://api.esimcenter.com/v3 | esk_ + 40 hex characters | Real orders, charged to your wallet or credit line. |
The key must match the host. A wrong pair is refused before anything else happens:
| You sent | To | You get |
|---|---|---|
A live key (esk_) | sandbox-api.esimcenter.com | 401 live_key_on_sandbox_host |
A sandbox key (esk_test_) | api.esimcenter.com | 401 sandbox_key_on_live_host |
| No key, an unknown key or a revoked key | either | 401 api_key_required or 401 invalid_api_key |
GET or HEAD with a 301 to esimcenter.com and every other method with 404.https://esimcenter.com/api/v3 still works and accepts both kinds of key; the key decides the mode there, as before. Existing integrations keep running.Every request needs your key in the X-API-Key header. Keys look like esk_ followed by 40 hex characters (sandbox: esk_test_), and we store only a hash of them.
X-API-Key: esk_3a4ef148ea886cbf51c5e2b1441e2b0de4b6ff66
Accept: application/jsonOnly the header is accepted. A request that carries the key as ?api_key= in the URL, or as an api_key field in the body, gets 401 api_key_header_required — even when the header is also present. Query strings end up in access logs, browser history and Referer headers, so treat a key that travelled in a URL as leaked and replace it from the developer panel.
Each key can carry an allow-list of source addresses, which you set yourself in the developer panel. Single addresses and CIDR blocks are accepted, IPv4 and IPv6, comma or newline separated — for example 203.0.113.7, 2001:db8::/32, 198.51.100.0/24. An empty list means no restriction, which is how every key starts.
A request from an address that is not on the list is refused with 403 ip_not_allowed before anything else happens — before the rate limiter, before the wallet, before any order. The refused address and the time are recorded on the key, so you can see in the panel whether the block was your own staging box or someone using a leaked key.
No key at all, a key we do not know, or a key whose account is closed — each is its own code, so you can tell a configuration mistake from a suspended account:
curl -s "$BASE/account/balance" \
-H "X-API-Key: $KEY"{
"result": "fail",
"error": "api_key_required",
"code": "401",
"title": "Unsuccessful",
"api_version": 3
}Every response is JSON with the same envelope, and the HTTP status always matches the code field.
{
"result": "ok",
"code": "200",
"balance": 42.16,
"title": "Successful",
"api_version": 3
}{
"result": "fail",
"error": "product_not_found",
"code": "404",
"title": "Unsuccessful",
"api_version": 3
}result is ok or fail, and code is the real HTTP status — you can branch on either.error is a stable machine code matching ^[a-z0-9_]+$. It is never translated and never a sentence. Branch on this, not on title, which is a human label we may reword at any time.YYYY-MM-DD HH:MM:SS in UTC. A date that is not set comes back as null, never as a zero date.v3; a breaking change would ship as v4 with notice.The complete list, grouped by where you meet them. Anything not on it is a bug on our side — please report it.
Retry policy we recommend. Retry 429 after the next minute boundary (or after Retry-After). Retry 500 once with the same idempotency_key. Never retry any other 4xx — the request itself is wrong and will fail again. The two 409 codes charge_in_progress and charge_incomplete are the exception: retry them with the same key.
| HTTP | error | When |
|---|---|---|
401 | api_key_required | The X-API-Key header is missing or empty. |
401 | api_key_header_required | The request carried an api_key in the URL (?api_key=) or in the body. Keys are accepted only in the X-API-Key header, so the request is refused even when that header is also present. |
401 | invalid_api_key | Unknown, revoked or expired key. |
401 | live_key_on_sandbox_host | A live key (esk_) was sent to sandbox-api.esimcenter.com. The sandbox host accepts sandbox keys only; use your live key on api.esimcenter.com. |
401 | sandbox_key_on_live_host | A sandbox key (esk_test_) was sent to api.esimcenter.com. Use it on sandbox-api.esimcenter.com, or create a live key for production. The old address esimcenter.com/api/v3 still accepts both kinds. |
403 | ip_not_allowed | The key has a source-address allow-list and this request did not come from it. |
403 | partner_not_found | The key is valid but its partner account no longer exists. |
403 | partner_disabled | The partner account has been suspended. |
404 | unknown_endpoint | No such resource under /api/v3. |
404 | unknown_method | The resource exists but has no such action. |
405 | method_not_allowed | Wrong HTTP verb. The response carries an "allow" field. |
429 | rate_limited | Over the per-minute ceiling of this endpoint's tier (see Rate limits). The response carries "limit_per_minute", "tier" and "retry_after", and the Retry-After header says how many seconds to wait. |
| HTTP | error | When |
|---|---|---|
400 | country_required | GET /catalog/products without ?country=. |
400 | invalid_country | Malformed country or region code. |
404 | country_not_found | The code is well formed but matches no country or region we sell. A country we do sell but have no plans for right now answers 200 with an empty products list instead. |
400 | invalid_usage_metered | The usage_metered filter was neither 0 nor 1. |
400 | product_id_required | Missing product id. |
400 | invalid_product | product_id could not be resolved, or a raw internal id was sent. |
404 | product_not_found | Unknown product id, or the product is no longer on sale. |
| HTTP | error | When |
|---|---|---|
400 | invalid_price | The product has no sellable price. |
402 | insufficient_balance | Prepaid mode: wallet balance below the price. The response carries "balance" and "required". |
403 | credit_limit_exceeded | Postpaid mode: this purchase would push your outstanding balance past your credit limit. The response carries "outstanding", "credit_limit", "available" and "required". |
403 | partner_account_suspended | Your partner account is suspended: every purchase is refused in both billing modes, before any order is created or any money moves. Balance, statement and order history stay readable. The response carries "billing_mode" and "status". |
402 | not_postpaid | A postpaid-only operation on an account that runs prepaid. |
409 | charge_in_progress | Another charge on your account is still being written. The order was NOT marked failed - retry with the same idempotency_key. |
409 | charge_incomplete | A previous charge stopped half-way and is being reconciled. The order was NOT marked failed - retry with the same idempotency_key. |
409 | idempotency_key_conflict | That idempotency_key was already used for a different product. |
400 | idempotency_key_too_long | Longer than 191 characters. We reject rather than truncate: two long keys sharing their first 191 characters would collapse two purchases into one order. A UUID is 36 characters. |
400 | invalid_idempotency_key | Not a plain string (an array or object), not valid UTF-8, or it contains a refused character: a 4-byte character such as an emoji, a double quote ("), a single quote ('), a backslash (\), <, >, or a control character (tab, line break, any other byte below 0x20, or 0x7F) inside the key; leading and trailing whitespace is trimmed first. Use letters, digits and - _ . : (a UUID or ULID fits; base64 with + / = works too). |
500 | sale_insert_failed | The order row could not be created. Retry with the same idempotency_key. |
| HTTP | error | When |
|---|---|---|
400 | order_id_required | Missing order id. |
404 | order_not_found | No such order under your account. |
500 | order_read_failed | GET /order/get or /order/detail/{id} could not read your orders this time. Nothing was changed; retry the same request later. Treat it as unknown, not as empty: when you really have no orders the answer is 200 with an empty "order", and a missing order is 404 order_not_found. |
400 | iccid_required | Missing ICCID. |
404 | usage_not_available | The ICCID is not yours, has no usage record, or the provider did not answer. |
404 | partner_account_not_found | GET /account/statement when we have never opened a billing account for you at all. Having one does not mean you are on invoice terms: a prepaid partner with an account gets 200 and zeros. Either way /account/balance, /account/history and /account/profitshare are the endpoints for a prepaid wallet. |
500 | history_read_failed | GET /account/history could not read your wallet movements this time. Nothing was changed; retry the same request later. Treat it as unknown, not as zero: when you really have no movements the answer is 200 with an empty "ledger". |
400 | amount_required | Missing or non-positive top-up amount. |
400 | amount_out_of_range | Top-up amount outside the accepted range. |
403 | topup_not_available | Card top-up is not enabled for this account. |
400 | payment_key_required | Missing payment key on confirm. |
404 | topup_not_found | No pending top-up for that payment key. |
400 | topup_failed | The payment provider rejected the intent. |
400 | topup_confirm_failed | The payment could not be confirmed. |
| HTTP | error | When |
|---|---|---|
404 | no_subscription | POST /webhook/test with no webhook endpoint registered for your account. |
404 | subscription_not_found | That subscription_id is not yours. |
409 | no_active_subscription | The subscription exists but is paused or disabled. |
409 | mode_immutable | A subscription's mode (live or sandbox) is fixed when it is created and cannot be changed. |
400 | probe_blocked | POST /webhook/probe to a URL that resolves to a private or link-local address. Refused before any request is opened; the response carries request_sent: false and a machine-readable reason. |
500 | test_enqueue_failed | The test event could not be queued. |
500 | webhook_read_failed | GET /webhook/list, GET /webhook/deliveries (the list or a single ?id=) or POST /webhook/test could not read your webhook data this time. Nothing was changed and no test event was queued; retry the same request later. Treat it as unknown, not as empty: when you really have no subscriptions or deliveries the answer is 200 with an empty list, and a test without any subscription is 404 no_subscription. |
| HTTP | error | When |
|---|---|---|
400 | sandbox_only_action | POST /account/sandboxReset with a live key. Resetting test data exists for sandbox keys only; nothing was deleted. |
400 | sandbox_unsupported_action | A sandbox key called an action the sandbox does not simulate (see the behaviour table under Sandbox). |
400 | invalid_sandbox_scenario | sandbox_scenario was not empty, success, deferred or failed. |
429 | sandbox_queue_full | Your account has reached the limit of sandbox webhook deliveries waiting to go out. POST /webhook/test is refused and the response carries "pending" and "limit"; a sandbox purchase still creates the order, but its events are not queued and the response carries "warnings": ["sandbox_queue_full"]. Space frees up as waiting deliveries go out. Live events are never limited. |
500 | sandbox_webhook_read_failed | The same as webhook_read_failed, for a sandbox key. GET /webhook/list, GET /webhook/deliveries or POST /webhook/test could not read your sandbox webhook data this time. Nothing was changed and no sandbox event was queued; retry the same request later. The response carries "livemode": false. |
Limits are per key and per tier, not one number for the whole API. Reading the catalog is cheap for us, so it gets a wide limit; spending money and making us call an address you chose get narrow ones. The table is generated from the limiter's own configuration.
| Tier | Limit | Endpoints |
|---|---|---|
catalogCatalog reads | 900 / min | catalog/countries catalog/products catalog/product |
readOrder & account reads | 360 / min | order/get order/detail order/usage account/balance account/history account/statement account/profitshare webhook/list webhook/deliveries |
purchasePurchase | 150 / min | purchase/buy account/sandboxReset |
moneyMoney movement | 30 / min | account/topup account/topupConfirm |
outboundOutbound calls we make for you | 18 / min | webhook/test webhook/probe |
The window slides: we weigh the previous minute in proportion to how much of it is still inside the last 60 seconds, so you cannot get a double burst by waiting for a clock minute to tick over. Every response carries your position:
X-RateLimit-Limit: 360
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 46X-RateLimit-Reset is seconds until the window rolls. Over the limit you get HTTP 429 with a Retry-After header and this body:
{
"result": "fail",
"error": "rate_limited",
"code": "429",
"tier": "read",
"limit_per_minute": 360,
"retry_after": 33,
"title": "Unsuccessful",
"api_version": 3
}The catalog changes a few times a day, not a few times a minute — cache /catalog/countries and /catalog/products on your side and you will never come close to the limit. Prices move with the daily refresh, so read the price at purchase time rather than storing it for weeks. If you need a higher quota for a launch, tell us in advance.
HTTP clients retry on timeout by default. Without an anchor, a retry is a second purchase — selling the same plan twice is legitimate for a reseller, so we cannot guess which one you meant. Send idempotency_key with every POST /purchase/buy: the key is unique per account at the database level, so even two requests arriving in the same millisecond collapse into one order and one deduction.
| You send | You get |
|---|---|
Same key, same product_id | The recorded outcome, replayed: 200 when delivered, 202 while still deferred, 200 with "delivery": "failed" when it ended in failure. Nothing is charged or provisioned a second time. |
Same key, different product_id | 409 idempotency_key_conflict. Generate a fresh key per intended purchase. |
| A new key after a failure | A second purchase, charged as one. Repeating the old key reads the failure back; it never retries. |
| No key | No protection. Two POSTs mean two eSIMs and two deductions. |
ABC and abc, or 0123 and 123, are different keys.- _ . :, up to 191 characters. Longer keys are rejected (idempotency_key_too_long), never truncated; refused characters give invalid_idempotency_key.402 insufficient_balance still consumes the key you sent. After topping up, retry with a new key.202 is "paid, arriving", not an error. Retrying it without your key is how resellers end up selling one customer two eSIMs.An order can be paid now and delivered minutes later, so you should not have to poll. Register one HTTPS endpoint and we POST every state change of your own orders to it, signed. Register it yourself in the developer panel; the signing secret is shown once at creation, and a lost one can only be replaced, never recovered.
| Event | When it fires | status |
|---|---|---|
order.created | The order row exists, payment not settled yet. | 3 |
order.paid | Payment (or the credit charge) went through; the eSIM is not provisioned yet. | 4 |
order.delivered | The eSIM is ready. iccid and qr_code are filled in. | 1 |
order.deferred | Paid, delivery postponed. delivery_reason says why; we keep retrying. | 4 |
order.failed | The order will not be delivered. | 2 |
order.test | You called /webhook/test. Does not correspond to a real order. | test |
POST https://your-endpoint.example.com/esimcenter
Content-Type: application/json
User-Agent: esimcenter-webhook/1.0
X-Webhook-Event: order.delivered
X-Webhook-Id: 41207
X-Webhook-Timestamp: 1790000000
X-Signature: sha256=9f86d0818...
{
"event": "order.delivered",
"event_id": "order.delivered:899991072:1",
"order_id": 899991072,
"status": "1",
"delivery": "delivered",
"delivery_reason": "",
"iccid": "8931088123456789012",
"qr_code": "LPA:1$rsp.truphone.com$QR-G-5C-1S-1TT7ETZ",
"occurred_at": "2026-09-25T04:12:07+03:00"
}delivery and delivery_reason carry the same values as the purchase response (see POST /purchase/buy) — one vocabulary for both. event and event_id are in the body as well as the headers, so a proxy that strips headers cannot make the payload ambiguous.qr_code is a bearer credential — whoever installs it first owns the line — so keep it out of your logs too.The signature covers the timestamp and the raw body, joined by a dot: sha256=hex(hmac_sha256(secret, timestamp + "." + body)). Hash the bytes you received — not a re-encoded version of the parsed JSON — and compare in constant time. Reject anything whose timestamp is more than five minutes off your clock: without that check a captured body could be replayed forever with its original signature.
<?php
$secret = getenv("ESIMCENTER_WEBHOOK_SECRET");
$body = file_get_contents("php://input");
$ts = $_SERVER["HTTP_X_WEBHOOK_TIMESTAMP"] ?? "";
$got = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
if (abs(time() - (int) $ts) > 300) {
http_response_code(400); // too old or too far in the future
exit;
}
$want = "sha256=" . hash_hmac("sha256", $ts . "." . $body, $secret);
if (!hash_equals($want, $got)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// Idempotent: remember $event["event_id"] and ignore one you have seen.
http_response_code(200); // 2xx = delivered, anything else is retried// Express: the RAW body is required, so mount this route before any JSON parser.
app.post("/esimcenter",
express.raw({ type: "application/json" }),
(req, res) => {
const ts = req.get("X-Webhook-Timestamp") || "";
const got = req.get("X-Signature") || "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const want = "sha256=" + crypto
.createHmac("sha256", process.env.ESIMCENTER_WEBHOOK_SECRET)
.update(Buffer.concat([Buffer.from(ts + "."), req.body]))
.digest("hex");
const a = Buffer.from(want), b = Buffer.from(got);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
// Idempotent: event_id is the key.
res.sendStatus(200);
});# Flask: request.get_data() returns the RAW bytes, which is what the signature covers.
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/esimcenter")
def esimcenter():
secret = os.environ["ESIMCENTER_WEBHOOK_SECRET"].encode()
body = request.get_data()
ts = request.headers.get("X-Webhook-Timestamp", "")
got = request.headers.get("X-Signature", "")
try:
age = abs(time.time() - int(ts))
except ValueError:
abort(400)
if age > 300:
abort(400) # too old or too far in the future
want = "sha256=" + hmac.new(secret, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, got):
abort(401)
event = request.get_json(force=True)
# Idempotent: remember event["event_id"] and ignore one you have seen.
return "", 200 # 2xx = delivered, anything else is retriedAny 2xx counts as delivered. Anything else — including a timeout — is retried on a fixed schedule:
| Attempt | Wait before the next one |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
event_id. Delivery is at-least-once: a response we never saw (your 200 lost in a timeout) is sent again. The same event_id always describes the same state change, so storing it and ignoring repeats is the whole of the work.order.paid can land after order.delivered. Use status / delivery in the payload as the truth, not the arrival order.| I want to… | Call |
|---|---|
| See my endpoints and the valid event names | GET /webhook/list — status is 1 active, 2 paused, 3 disabled after repeated failures. The secret is never returned. |
| Get a real signed test event | POST /webhook/test — queues an order.test on the real path (real signature, headers, retries). Expect it within a few minutes. |
| Find out why a webhook did not arrive | GET /webhook/deliveries — every attempt with the HTTP code we got, the duration and the start of your reply. With ?id= you also get the exact body we signed. |
| Try a URL before saving it | POST /webhook/probe — one signed request to any public URL, nothing saved, the whole exchange returned so you can reproduce the HMAC by hand. |
Outbound calls have their own, much smaller quota: webhook/test and webhook/probe share 18 requests per minute, because they make us open connections to addresses you choose. A probe the gate refuses still counts.
curl -s "$BASE/webhook/deliveries?id=27" \
-H "X-API-Key: $KEY"{
"result": "ok",
"code": "200",
"delivery": {
"id": "27",
"subscription_id": "10",
"event_id": "order.test:1655:1790594355",
"event_type": "order.test",
"sale_id": "0",
"status": "sent",
"attempts": "1",
"max_attempts": "5",
"http_code": "200",
"duration_ms": "17",
"response_body": "{\"ok\":true,\"received_bytes\":263,\"event\":\"order.test\"}",
"error": "",
"sent_at": "2026-09-28 14:20:05",
"date": "2026-09-28 14:19:15"
},
"payload": "{\"order_id\":0,\"status\":\"test\",\"delivery\":\"delivered\",\"delivery_reason\":\"\",\"iccid\":\"\",\"qr_code\":\"\",\"occurred_at\":\"2026-09-28T14:19:15+03:00\",\"note\":\"deneme olayi — gercek bir siparise karsilik gelmez\",\"event\":\"order.test\",\"event_id\":\"order.test:1655:1790594355\"}",
"payload_pruned": false,
"signature_rule": "X-Signature = 'sha256=' + HMAC_SHA256(<X-Webhook-Timestamp> + '.' + <gövdenin tamamı>, abonelik sırrı)",
"title": "Successful",
"api_version": 3
}Build and test your integration without real orders or charges. A sandbox key calls the same endpoints with the same field names on https://sandbox-api.esimcenter.com/v3. Every response the sandbox produces carries "livemode": false — errors included. Check it in your code before you trust a response. Authentication, method checks and rate limits work exactly as they do for live keys.
No money moves, no provider is called. A sandbox purchase does not touch your wallet or your credit line, does not create a real order, does not send email and does not earn revenue share. The eSIM it returns is fake and cannot be installed on a phone. Sandbox ICCIDs start with 89000, and activation codes point at sandbox.esimcenter.invalid.
| Endpoint | Sandbox behaviour |
|---|---|
catalog/* | The real catalog, same as live. Read only. |
account/pricing | Your real partner prices. Read only. |
account/balance | A fixed balance of 1000.00. Purchases do not lower it. |
account/history, statement, profitshare | Same fields as live, empty lists. The sandbox account is always prepaid. |
account/topup, topupConfirm | Simulated, see Simulated top-up. No card is charged. |
account/sandboxReset | Deletes your sandbox orders and their delivery records. See Resetting your sandbox data. |
purchase/buy | Creates a sandbox order. Pick the outcome with sandbox_scenario and the usage figures with sandbox_usage. |
order/get, detail | Only your sandbox orders. Failed sandbox orders are left out of the list, as failed live orders are. |
order/usage/{iccid} | Only the ICCID of your own delivered sandbox order. The figures follow the sandbox_usage you chose; the default is not_active, 1024 MB total, 0 MB used. |
webhook/list, deliveries, test | Only your sandbox subscriptions and their deliveries. Live subscriptions are not visible. |
webhook/probe | Works as live: one signed request to the URL you name, nothing saved. The body says "livemode": false. |
| Anything else | 400 sandbox_unsupported_action |
purchase/buy runs the same checks as live — the product must exist and the price is your real partner price — then creates a sandbox order instead of charging you. Send sandbox_scenario to pick what happens next. Live keys ignore the field.
sandbox_scenario | HTTP | delivery | Webhook events |
|---|---|---|---|
empty or success | 200 | delivered | order.created, order.paid, order.delivered |
deferred | 202 | deferred (awaiting_profile) | order.created, order.paid, order.deferred, then order.delivered 60 seconds later |
failed | 200 | failed | order.created, order.failed |
| anything else | 400 | invalid_sandbox_scenario | — |
curl -s -X POST "$BASE/purchase/buy" \
-H "X-API-Key: $KEY" \
-d "product_id=esc_1da490735cdc625a" \
-d "idempotency_key=1f7c0c9e-7b3a-4a6e-9f11-9a5b2d7c4e01" \
-d "sandbox_scenario=deferred"delivered the first time you read it after its 60 seconds are up — through order/detail, order/get or a retry with the same idempotency key. The order.delivered event is held back until the same moment.409 idempotency_key_conflict.To test a usage screen or a low-data alert you need an eSIM in every state. Add sandbox_usage to purchase/buy; order/usage/{iccid} of that order then reports the matching figures. Optional, defaults to not_active.
sandbox_usage | What order/usage reports |
|---|---|
not_active (default) | 1024 MB total, 0 MB used. The eSIM has not been installed yet. |
active | 1024 MB total, 512 MB used. |
depleted | 1024 MB total, 1024 MB used. Nothing is left. |
expired | The validity period is already over. |
curl -s -X POST "$BASE/purchase/buy" \
-H "X-API-Key: $KEY" \
-d "product_id=esc_1da490735cdc625a" \
-d "idempotency_key=7d5f0c64-52a1-4c0b-8c1e-3b8f9a2e6d10" \
-d "sandbox_usage=depleted"With a sandbox key, POST /v3/account/topup and POST /v3/account/topupConfirm run the same two-step flow as live, so you can build and test it end to end. Nothing reaches Stripe, your wallet or the orders table, and the sandbox balance stays 1000.00.
| Call | HTTP | What you get |
|---|---|---|
account/topup | 200 | A fake payment intent: id starting with sbx_topup_, client_secret starting with sbx_secret_, status requires_confirmation, "livemode": false. The secret is not a Stripe secret — do not hand it to Stripe's SDK. |
account/topupConfirm | 200 | status succeeded, "livemode": false. |
POST /v3/account/sandboxReset deletes the sandbox orders of your account together with their delivery records, so a test run can start from an empty list. It takes no parameters, only touches data of the key's own account, and never touches live orders. It counts against the purchase tier in Rate limits. A live key is refused with 400 sandbox_only_action and nothing is deleted.
curl -s -X POST "$BASE/account/sandboxReset" \
-H "X-API-Key: $KEY"{
"result": "ok",
"code": "200",
"deleted_orders": 4,
"livemode": false
}Sandbox data is not wiped on its own. Sandbox orders belong to your account, not to one key: revoking a sandbox key does not delete them, and your next sandbox key lists them again. Live keys never see them.
In the developer panel, pick Sandbox when you add a webhook endpoint. A sandbox subscription receives only the events of your sandbox orders and sandbox webhook/test calls; a live subscription never receives them. Sandbox events are signed and retried exactly like live ones, carry "livemode": false and have an event_id starting with sbx_.
409 mode_immutable if you try to change it. Add a second subscription in the other mode instead — the same URL may hold one live and one sandbox subscription.Every partner account runs in one of two modes. You do not choose it in the request — we set it on your account, and every purchase response tells you which one you are in via billing_mode.
billing_mode | How a purchase is paid | Not enough room |
|---|---|---|
prepaid | The price leaves your wallet balance immediately. This is the default. | 402 insufficient_balance with balance and required. Nothing was charged, no order exists. |
postpaid | No money moves. The price is written to your account as outstanding debt and settled against an invoice. balance does not change. | 403 credit_limit_exceeded with outstanding, credit_limit, available and required. |
403 partner_account_suspended, before any order is created or any money moves. Reading endpoints (balance, statement, orders) keep working.Two postpaid codes need the opposite reaction from every other error. 409 charge_in_progress and 409 charge_incomplete mean a charge on your account is half-written and we do not yet know whether the money moved. We deliberately do not mark the order failed — a false "failed" stamp would shut the recovery paths that fix it. Retry the same call with the same idempotency_key; you get either the finished order or a clean error.
Catalog prices are list prices. Your partner price is a per-product number derived from the list price, your share of our margin and the rules configured for your account, and it is the price your wallet is actually charged. The same catalog call with a different key can return different numbers.
| Field | Meaning |
|---|---|
price, sale_price | List price, what an end customer pays. Both stay equal to the list price for backwards compatibility. |
partner_price | What your wallet is charged for this product. Never negative and never below our cost; if our margin on the product is zero or unknown it equals the list price. |
partner_discount | The list price minus partner_price. "0.00" means no discount on this product, not "failed" or "missing". |
partner_margin_share_pct | The percentage of our margin turned into your discount on this row. "0.00" means the resolved share is zero. |
margin = catalog_price - cost_price # OUR margin on this product
discount = max(0, margin) * share_pct / 100 # your share of that margin
partner_price = list_price - discount # what your wallet is chargedThe discount is a percentage of our margin, not a flat percentage off the list price, so partner_price cannot fall below cost_price. The per-product percentage therefore varies; the average_discount_pct figure (with is_average: true) is a measurement, not a commitment — never quote an average to your customers as a guarantee.
Which rule wins — the most specific one, and source on the row says which: product (a fixed price for one product), then provider, country, region, then the account default, then none (no share configured; partner price equals list price). A fixed product price below the floor our margin rules permit is ignored at read time and resolution continues down the levels.
Total cap. Your account carries one cap, total_share_cap_pct (default 60%): the maximum share of our margin on a single product that can leave our side across both channels combined — the upfront discount here plus the unused-data share later on the same sale. When the cap binds, the unused-data share is clipped, never the upfront discount, and the revenue share row carries capped: 1.
On the sale row, product_price is the list price at purchase time and sale_price is the partner price actually charged; their difference is your upfront discount on that sale.
Countries and plans. Read-only; identical in both modes.
/catalog/countriesEvery destination we sell, one row per country code, sorted by code. Regions appear in the region field and can be passed to /catalog/products as a country value. Rate tier catalog: 900 requests per minute per key.
curl -s "https://sandbox-api.esimcenter.com/v3/catalog/countries" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/catalog/countries');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/catalog/countries', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/catalog/countries',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"count": 230,
"countries": [
{
"country_code": "AD",
"title": "Andorra",
"slug": "andorra",
"image": "https://esimcenter.com/cdn/flags/ad.png",
"region": "europe"
},
{
"country_code": "AE",
"title": "United Arab Emirates",
"slug": "united-arab-emirates",
"image": "https://esimcenter.com/cdn/flags/ae.png",
"region": "middle-east"
},
{
"country_code": "AG",
"title": "Antigua And Barbuda",
"slug": "antigua-and-barbuda",
"image": "https://esimcenter.com/cdn/flags/ag.png",
"region": "americas"
},
"<227 oge daha — ornek kisaltildi, gercek yanit tamamini doner>"
],
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 405 429
/catalog/productsThe same set, in the same order, that a customer sees on the website country page. By default one winning plan per country, days and data group is returned and filters run after that choice; pass all_variants=1 to filter across every variant. At most 500 plans per request; truncated is true when more matched. Every invalid filter value is refused with invalid_<name> instead of being ignored. Rate tier catalog: 900 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
country | query | string | required | ISO country code (de, tr, us) or region code/slug (europe, global, middle-east). Errors: country_required, invalid_country, country_not_found. |
type | query | string | optional | Optional catalog group filter, passed through to the selector the website uses. |
include_small | query | stringone of 0, 1 · default 0 | optional | 1 also returns packages under 1 GB. Off by default. |
all_variants | query | stringone of 0, 1 · default 0 | optional | 0: one winner per data/day group. 1: every variant. |
usage_metered | query | stringone of 0, 1 | optional | 1 only plans whose cost follows measured usage, 0 only the others, empty both. Error: invalid_usage_metered. |
unlimited | query | stringone of 0, 1 | optional | 1 only unlimited plans, 0 only capped plans, empty both. Error: invalid_unlimited. |
network_type | query | stringone of 4G, 5G | optional | Advertised network generation, case-insensitive. Error: invalid_network_type. |
min_data_mb | query | integermin 0 · max 10485760 | optional | Minimum bundled data in MB (1024 = 1 GB). Unlimited plans report amount 0, so any min_data_mb excludes them. Error: invalid_min_data_mb. |
max_data_mb | query | integermin 0 · max 10485760 | optional | Maximum bundled data in MB. Error: invalid_max_data_mb. |
day | query | integermin 1 · max 3650 | optional | Exact validity in days. Error: invalid_day. |
min_day | query | integermin 1 · max 3650 | optional | Minimum validity in days. Error: invalid_min_day. |
max_day | query | integermin 1 · max 3650 | optional | Maximum validity in days. Error: invalid_max_day. |
min_price | query | numbermin 0 · max 100000 | optional | Minimum price you pay, in USD. Error: invalid_min_price. |
max_price | query | numbermin 0 · max 100000 | optional | Maximum price you pay, in USD. Error: invalid_max_price. |
sort | query | stringone of price, price_per_gb, data, day, title · default price | optional | Sort field. Plans with no per-GB value are placed last in both directions when sorting by price_per_gb. Error: invalid_sort. |
order | query | stringone of asc, desc · default asc | optional | Sort direction. Error: invalid_order. |
limit | query | integermin 1 · max 500 · default 500 | optional | How many plans to return. Error: invalid_limit. |
offset | query | integermin 0 · max 999999 · default 0 | optional | How many matched plans to skip. Error: invalid_offset. |
curl -s "https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/catalog/products?country=tr',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"country_code": "tr",
"country": {
"country_code": "TR",
"title": "Turkey",
"slug": "turkey",
"image": "https://esimcenter.com/cdn/flags/tr.png",
"region": ""
},
"include_small": false,
"all_variants": false,
"total": 132,
"matched": 132,
"count": 132,
"truncated": false,
"limit": 500,
"offset": 0,
"sort": "price",
"order": "asc",
"filters": [],
"products": [
{
"id": "esc_0dde4d49725f9e60",
"title": "Turkey 1GB 7Days",
"country_code": "tr",
"country_title": "Turkey",
"country_image": "https://esimcenter.com/cdn/flags/tr.png",
"region": "country",
"continent": "",
"data": "1 GB",
"amount": 1024,
"day": 7,
"is_unlimited": "",
"price": 1.4299999999999999378275106209912337362766265869140625,
"sale_price": 1.4299999999999999378275106209912337362766265869140625,
"price_per_gb": 1.4299999999999999378275106209912337362766265869140625,
"operator_title": "",
"network_type": "5G",
"coverage_codes": [
"TR"
],
"coverage_names_en": [
"Turkey"
],
"fup_kind": "unknown",
"fup_daily_mb": 0,
"fup_speed_kbps": 0,
"fair_use_text": "",
"is_usage_metered": 0
},
{
"id": "esc_e7c9a2a773dcccba",
"title": "Turkey 3GB 15Days Premium",
"country_code": "tr",
"country_title": "Turkey",
"country_image": "https://esimcenter.com/cdn/flags/tr.png",
"region": "country",
"continent": "",
"data": "3 GB",
"amount": 3072,
"day": 15,
"is_unlimited": "",
"price": 1.6599999999999999200639422269887290894985198974609375,
"sale_price": 1.6599999999999999200639422269887290894985198974609375,
"price_per_gb": 0.5500000000000000444089209850062616169452667236328125,
"operator_title": "",
"network_type": "5G",
"coverage_codes": [
"TR"
],
"coverage_names_en": [
"Turkey"
],
"fup_kind": "unknown",
"fup_daily_mb": 0,
"fup_speed_kbps": 0,
"fair_use_text": "",
"is_usage_metered": 0
},
{
"id": "esc_93054d1eddb4d3e2",
"title": "Greece Cyprus Turkey 2GB 15d",
"country_code": "europe",
"country_title": "Europe",
"country_image": "https://esimcenter.com/cdn/flags/europe.png",
"region": "regional",
"continent": "Europe",
"data": "2 GB",
"amount": 2048,
"day": 15,
"is_unlimited": "",
"price": 2.680000000000000159872115546022541821002960205078125,
"sale_price": 2.680000000000000159872115546022541821002960205078125,
"price_per_gb": 1.3400000000000000799360577730112709105014801025390625,
"operator_title": "",
"network_type": "5G",
"coverage_codes": [
"CY",
"GR",
"TR"
],
"coverage_names_en": [
"Cyprus",
"Greece",
"Turkey"
],
"fup_kind": "unknown",
"fup_daily_mb": 0,
"fup_speed_kbps": 0,
"fair_use_text": "",
"is_usage_metered": 1
},
"<129 oge daha — ornek kisaltildi, gercek yanit tamamini doner>"
],
"title": "Successful",
"api_version": 3
}Status codes 200 400 401 403 404 405 429
/catalog/product/{id}One plan under a product key, same fields as the list. Works for variants that grouping hides from the list. Accepts only the opaque alias (esc_ + 16 hex); a raw internal id and an unknown alias both answer 404 product_not_found. Rate tier catalog: 900 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | stringpattern ^esc_[0-9a-f]{16}$ | required | Opaque product alias, esc_ + 16 lowercase hex characters. |
curl -s "https://sandbox-api.esimcenter.com/v3/catalog/product/esc_0dde4d49725f9e60" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/catalog/product/esc_0dde4d49725f9e60');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/catalog/product/esc_0dde4d49725f9e60', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/catalog/product/esc_0dde4d49725f9e60',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"product": {
"id": "esc_0dde4d49725f9e60",
"title": "Turkey 1GB 7Days",
"country_code": "tr",
"country_title": "Turkey",
"country_image": "https://esimcenter.com/cdn/flags/tr.png",
"region": "country",
"continent": "",
"data": "1 GB",
"amount": 1024,
"day": 7,
"is_unlimited": "",
"price": 1.4299999999999999378275106209912337362766265869140625,
"sale_price": 1.4299999999999999378275106209912337362766265869140625,
"price_per_gb": 1.4299999999999999378275106209912337362766265869140625,
"operator_title": "",
"network_type": "5G",
"coverage_codes": [
"TR"
],
"coverage_names_en": [
"Turkey"
],
"fup_kind": "unknown",
"fup_daily_mb": 0,
"fup_speed_kbps": 0,
"fair_use_text": "",
"is_usage_metered": 0
},
"title": "Successful",
"api_version": 3
}Status codes 200 400 401 403 404 405 429
The only endpoint that spends money (live) or creates a fake order (sandbox).
/purchase/buyLive key: checks the price, charges your wallet (prepaid) or credit line (postpaid) and orders the eSIM. Sandbox key: same input checks and your real partner price, then creates a sandbox order instead of charging you (balance stays 1000.00).
The response never carries the activation payload; read esim from GET /order/detail/{id}.
200 delivered: the eSIM exists. 202 deferred: paid, arriving later, do NOT buy again; wait for the order.delivered webhook or poll order/detail. 200 failed: the order will not be delivered.
Send your own unique idempotency_key per intended purchase. The same key with the same product returns the recorded result and moves no money; with another product it is 409. To retry a failed order, send a NEW key.
Rate tier purchase: 150 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
product_id | body (form) | stringpattern ^esc_[0-9a-f]{16}$ | required | Opaque, stable, irreversible product alias. Raw internal ids are refused. |
idempotency_key | body (form) | stringmax length 191 | optional | Strongly recommended. At most 191 bytes after trimming; no quotes, backslash, < >, control characters or 4-byte characters. A UUID fits. Errors: idempotency_key_too_long, invalid_idempotency_key. |
sandbox_scenario | body (form) | stringone of success, deferred, failed | optional | Sandbox keys only; live keys ignore it. Empty means success. Any other value is 400 invalid_sandbox_scenario. |
sandbox_usage | body (form) | stringone of not_active, active, depleted, expired · default not_active | optional | Sandbox keys only. Decides what order/usage/{iccid} reports for the fake eSIM of this order. |
curl -s -X POST "https://sandbox-api.esimcenter.com/v3/purchase/buy" \
-H "X-API-Key: esk_test_YOUR_KEY" \
-d "product_id=esc_1da490735cdc625a" \
-d "idempotency_key=1f7c0c9e-7b3a-4a6e-9f11-9a5b2d7c4e01"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/purchase/buy');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query([
'product_id' => 'esc_1da490735cdc625a',
'idempotency_key' => '1f7c0c9e-7b3a-4a6e-9f11-9a5b2d7c4e01',
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/purchase/buy', {
method: 'POST',
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
body: new URLSearchParams({
product_id: 'esc_1da490735cdc625a',
idempotency_key: '1f7c0c9e-7b3a-4a6e-9f11-9a5b2d7c4e01',
}),
});
const data = await res.json();
import requests
r = requests.post(
'https://sandbox-api.esimcenter.com/v3/purchase/buy',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
data={
'product_id': 'esc_1da490735cdc625a',
'idempotency_key': '1f7c0c9e-7b3a-4a6e-9f11-9a5b2d7c4e01',
},
)
data = r.json()
HTTP 200 example
{
"result": "ok",
"code": "200",
"id": "2470",
"order_id": 2470,
"billing_mode": "prepaid",
"balance": 3.939999999999999946709294817992486059665679931640625,
"delivery": "delivered",
"delivery_reason": "",
"provision": "ok",
"order": {
"id": "2470",
"product_id": "esc_1da490735cdc625a",
"product_title": "Turkey 100MB 7Days",
"product_price": "1.06",
"sale_price": "1.06",
"coupon_code": "",
"product_type": "esim",
"date": "2026-09-28 13:06:09",
"status": "1",
"api_sale_id": "B26092810060019",
"api_sale_code": "26092810060022"
},
"title": "Successful",
"api_version": 3
}Status codes 200 202 400 401 402 403 404 405 409 429 500
Order list, order detail with activation data, and data usage.
/order/getYour orders, newest first, under the singular key order. Sandbox keys see only their own sandbox orders; failed sandbox orders are left out, as failed live orders are. Rate tier read: 360 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integerdefault 10 | optional | Page size. |
start | query | integermin 1 · default 1 | optional | 1-based page number. |
curl -s "https://sandbox-api.esimcenter.com/v3/order/get" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/order/get');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/order/get', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/order/get',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"order": [
{
"id": "2471",
"product_id": "esc_e4c66b73d7954ed2",
"product_title": "Algeria 100MB 7Days",
"title_display": "100 MB · 7 days",
"product_price": "1.06",
"sale_price": "1.06",
"product_type": "esim",
"date": "2026-09-28 13:06:58",
"status": "1",
"api_sale_id": "B26092810070001",
"api_sale_code": "26092810070001",
"esim": {
"qrcode_url": "",
"iccid": "<maskelendi>",
"lpa": "<maskelendi>",
"smdpAddress": "<maskelendi>",
"activationCode": "<maskelendi>",
"matching_id": null,
"direct_apple_installation_url": null
},
"data": "100 MB",
"day": "7",
"is_unlimited": "",
"country_code": "dz",
"country_image": "https://esimcenter.com/cdn/flags/dz.png",
"country_name": "Algeria",
"fair_use_text": "",
"coupon_code": ""
}
],
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 405 429 500
/order/detail/{id}One order in full. The esim block carries the activation details (lpa, smdpAddress, activationCode, matching_id); treat it as a secret. An id that is not yours answers 404 order_not_found, the same as an id that does not exist. Rate tier read: 360 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integermin 1 | required | Order id (the id / order_id returned by purchase/buy). |
curl -s "https://sandbox-api.esimcenter.com/v3/order/detail/2470" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/order/detail/2470');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/order/detail/2470', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/order/detail/2470',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"order": {
"id": "2471",
"product_id": "esc_e4c66b73d7954ed2",
"product_title": "Algeria 100MB 7Days",
"title_display": "100 MB · 7 days",
"product_price": "1.06",
"sale_price": "1.06",
"coupon_code": "",
"date": "2026-09-28 13:06:58",
"status": "1",
"api_sale_id": "B26092810070001",
"api_sale_code": "26092810070001",
"data": "100 MB",
"day": "7",
"is_unlimited": false,
"voice": "",
"text": "",
"short_info": "Algeria 100MB 7Days",
"fair_use_text": "",
"fup_kind": "unknown",
"fup_daily_mb": 0,
"fup_speed_kbps": 0,
"operator_title": "",
"operator_image": "https://esimcenter.com/cdn/assets/images/providers/<maskelendi>.jpg",
"country_code": "dz",
"country_image": "https://esimcenter.com/cdn/flags/dz.png",
"country_name": "Algeria"
},
"esim": {
"qrcode": null,
"qrcode_url": "",
"iccid": "<maskelendi>",
"lpa": "<maskelendi>",
"matching_id": null,
"smdpAddress": "<maskelendi>",
"activationCode": "<maskelendi>",
"confirmation_code": null,
"msisdn": null,
"apn": "plus",
"apn_type": null,
"apn_value": null,
"is_roaming": null
},
"operator_meta": {
"activation_policy": null,
"plan_type": "data",
"esim_type": "Prepaid",
"is_roaming": null,
"is_kyc_verify": null,
"rechargeability": null,
"apn_type": null,
"apn_value": null,
"info": null,
"other_info": null,
"warning": null
},
"title": "Successful",
"api_version": 3
}Status codes 200 400 401 403 404 405 429 500
/order/usage/{iccid}Remaining data. Cache it for a minute or two. 404 usage_not_available means the ICCID is not yours, has no usage record yet, or the provider did not answer; it is not an integration error. With a sandbox key only the ICCID of your own delivered sandbox order works, and the figures follow the sandbox_usage chosen at purchase: not_active (default) 1024 MB total and 0 used, active 1024 total and 512 used, depleted 1024 of 1024 used, expired with an expiry date in the past. Rate tier read: 360 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
iccid | path | string | required | ICCID of the eSIM. Sandbox ICCIDs start with 89000. |
curl -s "https://sandbox-api.esimcenter.com/v3/order/usage/8900000000000000001" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/order/usage/8900000000000000001');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/order/usage/8900000000000000001', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/order/usage/8900000000000000001',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"usage": {
"iccid": "<maskelendi>",
"sale_id": 2470,
"title": "Turkey 100MB 7Days",
"status": "not_active",
"tone": "ok",
"total_mb": 100,
"used_mb": 0,
"remaining_mb": 100,
"percent_remaining": 100,
"is_unlimited": false,
"fair_use_text": "",
"activated_at": null,
"expires_at": null,
"days_left": null,
"fetched_at": "2026-09-28 14:00:03"
},
"title": "Successful",
"api_version": 3
}Status codes 200 400 401 403 404 405 429
Wallet, statement, revenue share, pricing and top-up.
/account/balanceSpendable balance in USD. Sandbox keys always see 1000.00, and purchases do not reduce it. On invoice terms read account/statement instead. Rate tier read: 360 requests per minute per key.
curl -s "https://sandbox-api.esimcenter.com/v3/account/balance" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/balance');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/balance', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/account/balance',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"balance": 0,
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 405 429
/account/historyWallet movements, newest first, each with the balance that resulted. Sandbox keys get an empty ledger. A read failure is 500 history_read_failed, which means unknown, not empty. Rate tier read: 360 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integermin 1 · max 500 · default 50 | optional | How many movements. Values above 500 are clamped. |
start | query | integermin 1 · default 1 | optional | 1-based page number. |
curl -s "https://sandbox-api.esimcenter.com/v3/account/history" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/history');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/history', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/account/history',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"ledger": [],
"balance": 0,
"balances": {
"balance": 0,
"promo": 0,
"cash": 0,
"debt": 0
},
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 405 429 500
/account/statementBilling mode, credit limit, outstanding debt, remaining credit (available may be negative) and credit-line movements. 404 partner_account_not_found when no billing account was ever opened for you. Sandbox keys see a prepaid account with zeros and an empty ledger. Rate tier read: 360 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integerdefault 50 | optional | How many movements per page. Default 50, at most 500. |
start | query | integermin 1 · default 1 | optional | 1-based page number. |
type | query | stringone of charge, payment, adjust, refund, profit_share | optional | Optional movement type filter: charge, payment, adjust, refund or profit_share. Any other value is ignored. |
curl -s "https://sandbox-api.esimcenter.com/v3/account/statement" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/statement');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/statement', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/account/statement',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"billing_mode": "postpaid",
"credit_limit": "5.00",
"outstanding": "1.06",
"available": "3.94",
"profit_share_enabled": 1,
"status": 1,
"currency": "USD",
"ledger": [
{
"id": "1",
"type": "charge",
"amount": "1.06",
"balance_after": "1.06",
"note": "eSIM purchase #2471",
"date": "2026-09-28 13:06:58"
}
],
"count": 1,
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 404 405 429
/account/pricingYour contract numbers: margin share, total share cap, number of active special price rules, and a measured average discount. average_discount_pct is a measurement, not a promise: null means it could not be measured, 0 means measured with no discount. Sandbox keys get the same live answer. Rate tier read: 360 requests per minute per key.
curl -s "https://sandbox-api.esimcenter.com/v3/account/pricing" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/pricing');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/pricing', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/account/pricing',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
Status codes 200 401 403 405 429
/account/topupLive key: creates a Stripe payment intent; you complete the payment with Stripe's own SDK using the returned client_secret, then call topupConfirm with the payment_key. Sandbox key: returns a fake intent (id starting sbx_topup_, client_secret starting sbx_secret_, status: requires_confirmation, livemode: false); no Stripe call is made and the balance stays 1000.00. Rate tier money: 30 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
amount | body (form) | number> 0 | required | Amount credited to the wallet, in USD. Outside the accepted range: 400 amount_out_of_range (carries min and max). |
curl -s -X POST "https://sandbox-api.esimcenter.com/v3/account/topup" \
-H "X-API-Key: esk_test_YOUR_KEY" \
-d "amount=50"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/topup');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query([
'amount' => '50',
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/topup', {
method: 'POST',
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
body: new URLSearchParams({
amount: '50',
}),
});
const data = await res.json();
import requests
r = requests.post(
'https://sandbox-api.esimcenter.com/v3/account/topup',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
data={
'amount': '50',
},
)
data = r.json()
Status codes 200 400 401 403 405 429
/account/topupConfirmVerifies the payment and credits the wallet. Idempotent: confirming twice credits once. Sandbox key: returns succeeded without touching any wallet. Rate tier money: 30 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
payment_key | body (form) | string | required | The payment_key returned by topup. Sandbox keys return keys that start with sbx_topup_. |
curl -s -X POST "https://sandbox-api.esimcenter.com/v3/account/topupConfirm" \
-H "X-API-Key: esk_test_YOUR_KEY" \
-d "payment_key=sbx_topup_0123456789abcdef01234567_5000"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/topupConfirm');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query([
'payment_key' => 'sbx_topup_0123456789abcdef01234567_5000',
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/topupConfirm', {
method: 'POST',
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
body: new URLSearchParams({
payment_key: 'sbx_topup_0123456789abcdef01234567_5000',
}),
});
const data = await res.json();
import requests
r = requests.post(
'https://sandbox-api.esimcenter.com/v3/account/topupConfirm',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
data={
'payment_key': 'sbx_topup_0123456789abcdef01234567_5000',
},
)
data = r.json()
Status codes 200 400 401 403 404 405 429
Read your webhook subscriptions and delivery log, and test an address. Subscriptions are created in the developer panel.
/webhook/listYour subscriptions of the key's own mode: sandbox keys see only sandbox subscriptions, live keys only live ones. The signing secret is never returned, only secret_mask. Subscriptions are created in the developer panel. Rate tier read: 360 requests per minute per key.
curl -s "https://sandbox-api.esimcenter.com/v3/webhook/list" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/webhook/list');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/webhook/list', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/webhook/list',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"subscriptions": [
{
"id": "10",
"url": "https://esimcenter.com/bin/olc/wh-alici.php?t=db4e55c7ea6a9a8b80e050e828731a02&p=p2",
"secret_mask": "402c11...",
"events_list": [
"order.created",
"order.paid",
"order.delivered",
"<3 oge daha — ornek kisaltildi, gercek yanit tamamini doner>"
],
"status": "1",
"fail_count": "0",
"date": "2026-09-28 12:44:26",
"updated_at": "2026-09-28 13:39:09"
}
],
"count": 1,
"events": [
"order.created",
"order.paid",
"order.delivered",
"<3 oge daha — ornek kisaltildi, gercek yanit tamamini doner>"
],
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 405 429 500
/webhook/deliveriesWithout id the latest deliveries (without body). With id one delivery with the exact body we sent in payload and the signature_rule. Sandbox keys see only deliveries of sandbox subscriptions. Rate tier read: 360 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | query | integermin 1 | optional | Delivery id. When present, the single-delivery mode is used and the other filters are ignored. |
status | query | string | optional | Delivery status filter. The accepted values are returned in statuses; others answer 400 invalid_status. |
event | query | string | optional | Event filter, one of events; others answer 400 invalid_event. |
subscription_id | query | stringpattern ^\d{1,20}$ | optional | Digits only; otherwise 400 invalid_subscription_id. |
limit | query | integermin 1 · max 200 · default 50 | optional | 1 to 200. Otherwise 400 invalid_limit. |
curl -s "https://sandbox-api.esimcenter.com/v3/webhook/deliveries" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/webhook/deliveries');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/webhook/deliveries', {
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.get(
'https://sandbox-api.esimcenter.com/v3/webhook/deliveries',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"count": 3,
"limit": 3,
"statuses": [
"pending",
"sent",
"failed",
"<1 oge daha — ornek kisaltildi, gercek yanit tamamini doner>"
],
"events": [
"order.created",
"order.paid",
"order.delivered",
"<3 oge daha — ornek kisaltildi, gercek yanit tamamini doner>"
],
"deliveries": [
{
"id": "27",
"subscription_id": "10",
"event_id": "order.test:1655:1790594355",
"event_type": "order.test",
"sale_id": "0",
"status": "sent",
"attempts": "1",
"max_attempts": "5",
"http_code": "200",
"duration_ms": "17",
"response_body": "{\"ok\":true,\"received_bytes\":263,\"event\":\"order.test\"}",
"error": "",
"sent_at": "2026-09-28 14:20:05",
"date": "2026-09-28 14:19:15"
},
{
"id": "26",
"subscription_id": "10",
"event_id": "order.test:1655:1790594353",
"event_type": "order.test",
"sale_id": "0",
"status": "sent",
"attempts": "1",
"max_attempts": "5",
"http_code": "200",
"duration_ms": "12",
"response_body": "{\"ok\":true,\"received_bytes\":263,\"event\":\"order.test\"}",
"error": "",
"sent_at": "2026-09-28 14:20:04",
"date": "2026-09-28 14:19:13"
},
{
"id": "25",
"subscription_id": "10",
"event_id": "order.test:1655:1790594270",
"event_type": "order.test",
"sale_id": "0",
"status": "sent",
"attempts": "1",
"max_attempts": "5",
"http_code": "200",
"duration_ms": "20",
"response_body": "{\"ok\":true,\"received_bytes\":263,\"event\":\"order.test\"}",
"error": "",
"sent_at": "2026-09-28 14:20:04",
"date": "2026-09-28 14:17:50"
}
],
"title": "Successful",
"api_version": 3
}Status codes 200 400 401 403 404 405 429 500
/webhook/testQueues a signed order.test event to your registered subscriptions of the key's mode. Delivery happens asynchronously; read the result from webhook/deliveries. Sandbox keys: only sandbox subscriptions, event id starts with sbx_; when your sandbox queue is full the answer is 429 sandbox_queue_full. Rate tier outbound: 18 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
subscription_id | body (form) | integer | optional | Target one subscription. Omit to target all your active subscriptions. Not yours: 404 subscription_not_found. |
curl -s -X POST "https://sandbox-api.esimcenter.com/v3/webhook/test" \
-H "X-API-Key: esk_test_YOUR_KEY" \
-d "subscription_id=10"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/webhook/test');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query([
'subscription_id' => '10',
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/webhook/test', {
method: 'POST',
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
body: new URLSearchParams({
subscription_id: '10',
}),
});
const data = await res.json();
import requests
r = requests.post(
'https://sandbox-api.esimcenter.com/v3/webhook/test',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
data={
'subscription_id': '10',
},
)
data = r.json()
HTTP 200 real captured response
{
"result": "ok",
"code": "200",
"event": "order.test",
"event_id": "order.test:1655:1790594918",
"queued": 1,
"subscriptions": 1,
"delivery_mode": "queued",
"title": "Successful",
"api_version": 3
}Status codes 200 401 403 404 405 409 429 500
/webhook/probeSends one signed request to the given address right now and returns the HTTP status, latency and a trimmed response. Nothing is saved. A fresh single-use secret is generated and returned as probe_secret so you can test your signature check. Internal and private addresses are refused with 400 probe_blocked and no request is sent. Same behavior for both key kinds; the livemode field in the sent body follows the key's mode. Rate tier outbound: 18 requests per minute per key.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
url | body (form) | stringmax length 500 | required | https address to test. Errors: url_required, url_too_long, probe_blocked. |
event | body (form) | stringdefault order.test | optional | Event name to simulate; one of the events returned by webhook/list. Otherwise 400 invalid_event. |
curl -s -X POST "https://sandbox-api.esimcenter.com/v3/webhook/probe" \
-H "X-API-Key: esk_test_YOUR_KEY" \
-d "url=https://esimcenter.com/" \
-d "event=order.paid"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/webhook/probe');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => http_build_query([
'url' => 'https://esimcenter.com/',
'event' => 'order.paid',
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/webhook/probe', {
method: 'POST',
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
body: new URLSearchParams({
url: 'https://esimcenter.com/',
event: 'order.paid',
}),
});
const data = await res.json();
import requests
r = requests.post(
'https://sandbox-api.esimcenter.com/v3/webhook/probe',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
data={
'url': 'https://esimcenter.com/',
'event': 'order.paid',
},
)
data = r.json()
HTTP 200 example
{
"result": "ok",
"code": "200",
"request_sent": true,
"saved": false,
"url": "https://esimcenter.com/",
"event": "order.paid",
"event_id": "probe.order.paid:1001:1790594918",
"http_status": 200,
"accepted": true,
"duration_ms": 120,
"response_body": "<!DOCTYPE html>\n<html lang=\"en\" dir=\"ltr\">\n<head><meta charset=\"utf-8\">…",
"payload": "{\"order_id\":0,\"status\":\"test\",\"delivery\":\"delivered\",\"delivery_reason\":\"\",\"iccid\":\"\",\"qr_code\":\"\",\"occurred_at\":\"2026-09-28T14:28:38+03:00\",\"event\":\"order.paid\",\"event_id\":\"probe.order.paid:1001:1790594918\"}",
"timestamp": 1790594918,
"signature": "<redacted>",
"probe_secret": "<redacted>",
"signature_rule": "X-Signature = 'sha256=' + HMAC_SHA256(timestamp + '.' + payload, probe_secret)",
"title": "Successful",
"api_version": 3,
"error": ""
}Status codes 200 400 401 403 405 429 500
Endpoints and fields that exist only for sandbox keys.
/account/sandboxResetSandbox keys only. Deletes the sandbox orders and delivery records of your account so you can start a test run from a clean state. A live key gets 400 sandbox_only_action and nothing is touched. Rate tier purchase: 150 requests per minute per key.
curl -s -X POST "https://sandbox-api.esimcenter.com/v3/account/sandboxReset" \
-H "X-API-Key: esk_test_YOUR_KEY"<?php
$ch = curl_init('https://sandbox-api.esimcenter.com/v3/account/sandboxReset');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: esk_test_YOUR_KEY'],
CURLOPT_CUSTOMREQUEST => 'POST',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const res = await fetch('https://sandbox-api.esimcenter.com/v3/account/sandboxReset', {
method: 'POST',
headers: { 'X-API-Key': 'esk_test_YOUR_KEY' },
});
const data = await res.json();
import requests
r = requests.post(
'https://sandbox-api.esimcenter.com/v3/account/sandboxReset',
headers={'X-API-Key': 'esk_test_YOUR_KEY'},
)
data = r.json()
HTTP 200 example
{
"result": "ok",
"code": "200",
"deleted_orders": 4,
"livemode": false
}Status codes 200 400 401 403 405 429
Import the collection and one environment into Postman, or feed the OpenAPI file to your client generator. The sandbox environment points at https://sandbox-api.esimcenter.com/v3, the live one at https://api.esimcenter.com/v3; switch between them with the environment picker, and put your key into the environment's key variable.
esimcenter-api-v3.openapi.jsonesimcenter-api-v3.postman_collection.jsonesimcenter-sandbox.postman_environment.jsonesimcenter-live.postman_environment.json/order/detail/{id} after any purchase that came back 202. Do not poll faster than once every 30 seconds — it counts against your rate limit and nothing changes that quickly./webhook/list afterwards.esk_test_ runs the same endpoints without charging you or calling a provider — see Sandbox.v3; a breaking change would ship as v4 with notice.Write to us with what you are building and the volume you expect. We answer integration questions from engineers directly.
Contact us
How was your experience?
Review us on Trustpilot