Sandbox
With a test key, every endpoint in this reference works against the sandbox. One endpoint exists only in the sandbox. It lets you make a test eSIM behave as if a traveller were using it, so you can test your webhook handling without waiting.
The Sandbox guide covers the rest: the virtual balance and the customer_ref values that trigger failures.
Simulate an eSIM event
Section titled “Simulate an eSIM event”POST /v1/sandbox/esims/{iccid}/simulate
Updates a sandbox eSIM and sends the matching webhook to your test endpoints.
The eSIM moves through the same life as a real one, and the events must come in that order:
ready --activated--> active --usage_100--> depleted --expired--> expired active --expired--> expiredThis endpoint needs a test key. With a live key it answers 404.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
iccid |
path | string | Yes | The ICCID of a sandbox eSIM. It starts with 8999999. |
Idempotency-Key |
header | string | Yes | 1 to 255 characters. Use a new value for each simulation. A repeated value returns the first response and does nothing. |
event |
body | string | Yes | activated, usage_80, usage_100 or expired. |
event |
Allowed when the eSIM is | What changes | Webhook sent |
|---|---|---|---|
activated |
ready |
status becomes active. activated_at is set to now and expires_at to now plus validity_days. |
esim.activated |
usage_80 |
active |
data.used_mb becomes 80% of data.total_mb, rounded up. |
esim.usage_80 |
usage_100 |
active |
data.used_mb becomes data.total_mb and status becomes depleted. |
esim.usage_100 |
expired |
active or depleted |
status becomes expired and expires_at is set to now. |
esim.expired |
Rules:
- An event the eSIM’s status does not allow is refused with
409and the codeinvalid_state: a usage event orexpiredon areadyeSIM, and anything butexpiredon anexpiredeSIM. - An event that changes nothing returns the eSIM as it is and sends no webhook:
activatedon an eSIM that is already active,expiredon one that is already expired, or a usage level that was already reached. Usage never goes down. - After a top-up, a
depletedeSIM isactiveagain and the usage events can be simulated once more. - Usage events cannot be simulated for an unlimited eSIM.
curl -X POST "$ESIMIFY_API_URL/v1/sandbox/esims/8999999000000012345/simulate" \ -H "Authorization: Bearer $ESIMIFY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: sim-activated-001" \ -d '{ "event": "activated" }'const res = await fetch(`${process.env.ESIMIFY_API_URL}/v1/sandbox/esims/8999999000000012345/simulate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.ESIMIFY_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'sim-activated-001', }, body: JSON.stringify({ event: 'activated', }),});const esim = await res.json();<?php$ch = curl_init(getenv('ESIMIFY_API_URL') . '/v1/sandbox/esims/8999999000000012345/simulate');curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('ESIMIFY_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: sim-activated-001', ], CURLOPT_POSTFIELDS => json_encode([ 'event' => 'activated', ]),]);$esim = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);import osimport requests
res = requests.post( os.environ["ESIMIFY_API_URL"] + "/v1/sandbox/esims/8999999000000012345/simulate", headers={ "Authorization": "Bearer " + os.environ["ESIMIFY_API_KEY"], "Idempotency-Key": "sim-activated-001", }, json={ "event": "activated", }, timeout=30,)esim = res.json()Response
Section titled “Response”200 OK with the updated Esim. A replay also answers 200 OK.
{ "iccid": "8999999000000012345", "status": "active", "mode": "test", "order_id": "ord_test_3f9a1c2b7d4e", "customer_ref": "booking-84512", "package": { "id": "pkg_10482", "name": "Thailand 1 GB 7 days" }, "qr_code_url": "https://api.example.com/partner/api/v1/qr/ODk5OTk5OTAwMDAwMDAxMjM0NQ.3f9a1c2b5e8d7c6b4a39281706f5e4d3c2b1a098.png", "activation": { "lpa": "LPA:1$sandbox.esimify.in$TEST-A1B2C3D4E5F6", "smdp_address": "sandbox.esimify.in", "activation_code": "TEST-A1B2C3D4E5F6" }, "install_links": { "ios": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-A1B2C3D4E5F6", "android": "https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24sandbox.esimify.in%24TEST-A1B2C3D4E5F6" }, "data": { "unlimited": false, "total_mb": 1024, "used_mb": 0, "remaining_mb": 1024 }, "validity_days": 7, "activated_at": "2026-10-02T11:02:40Z", "expires_at": "2026-10-09T11:02:40Z", "created": "2026-10-02T09:14:07Z"}Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
400 |
invalid_request |
event is missing or is not one of the four values, or a usage event was sent for an unlimited eSIM. param is event. |
400 |
idempotency_key_required |
A POST was sent without an Idempotency-Key header. |
404 |
not_found |
The endpoint was called with a live key. |
404 |
esim_not_found |
No sandbox eSIM has this ICCID for your account. |
409 |
idempotency_key_reused |
This Idempotency-Key was already used with a different request body. |
409 |
invalid_state |
The event does not fit the eSIM’s current status. Send activated first. |
Any endpoint can also return the common errors.

