Skip to main content
Once an eSIM is issued, we POST its activation data to your webhook URL as application/json. Return any 2xx to acknowledge it. One unit per webhook. A product ordered with qty: 2 produces two separate deliveries, each of which may be retried. Match each to your order with external_order_id, and to the unit with topup_id. Receiving the webhook means the eSIM exists and can be handed to your customer. It does not mean the plan has started — that happens when the profile first connects to a network at the destination, which is usually days later. See Installed is not activated. The same URL also receives refund decisions, if you request refunds through the API. Tell them apart by the event field that leads every body: Verify the signature, then branch on event. The X-Esim-Story-Event header carries the same value, so you can route a delivery before parsing it — but only the body is signed, so decide on the body field. We may add events; ignore and acknowledge any you do not handle, with a 2xx, or they will be retried. Every delivery is signed. Verify the signature before you process the payload — without that check, anyone who learns your webhook URL can post fabricated eSIM data to it.
Delivery is at-least-once, so expect the same topup_id twice. If your endpoint is down, times out, or returns a non-2xx, we retry up to 10 times with exponential backoff — 11 attempts in all, spread over roughly four and a half hours. A delivery you processed but acknowledged too slowly will arrive again. Make your handler idempotent: for esim.issued, topup_id is unique per unit and never reused, so it is the natural deduplication key. A refund event carries the same topup_id as the eSIM it is about, so deduplicate on the event and topup_id together.Acknowledge fast — enqueue the payload and process it asynchronously rather than doing work before responding. We wait 10 seconds for your response.If all attempts fail, the delivery is logged on our side and we stop. Contact support to recover the eSIM data. A missed refund decision can be read back with Get a refund request.

Payload

string
Always esim.issued for an eSIM delivery. See the event table above.
string
Matches a topup_id from the order creation response.
string
The option ID that was ordered.
string
The external_order_id you supplied when creating the order.
string
ICCID of the issued eSIM profile.
string
SM-DP+ server address for the profile.
string
Matching ID / activation code for the profile.
Full LPA activation string, formatted LPA:1$<smdp>$<activate_code>. Use for manual / universal-link activation where scanning a QR code is not possible.
string
Hosted QR code image for download_link, provided as a convenience. Use the URL exactly as provided.Treat it as opaque: do not parse it, and do not assume the host stays the same. Where we host these images may change without notice. download_link is the durable value — the QR encodes that string and nothing else, so you can always render your own.In sandbox every order returns the same placeholder image. Identify a unit by topup_id, never by this URL.
Rendering the QR code yourself — recommended.The QR encodes download_link and nothing else. This is fixed by the GSMA eSIM specification (SGP.22): the device’s LPA reads the activation string out of the image, so any QR generated from the same string is byte-equivalent in effect, on iOS, Android and anything else implementing the spec.That means you do not have to link to our image at all. Render it in your own stack, serve it from your own domain, and your customer sees only your brand from purchase through activation — which is the point of integrating at the API level rather than sending them to a storefront. It also removes a third-party host from your activation flow: one less dependency between your customer and a working eSIM.
Use these parameters. Error correction M, a 4-module quiet zone, and at least 8 px per module (~330 px for a typical activation string). The quiet zone is the blank margin around the code — the QR specification requires four modules of it, and cameras fail to lock on when it is thinner. A cramped or undersized QR is the most common cause of “the code will not scan”, and it surfaces as a support ticket long after the order succeeded.Do not overlay a logo on the code area. Never re-encode a screenshot of a rendered QR — generate from download_link every time.
string
Date (YYYY-MM-DD) after which the profile can no longer be installed.

Example payload

Refund events

Sent once our team decides a refund request. The body is the refund request — the same fields Request a refund returns — with event in front, as on every webhook.
Each request is decided once, so expect one refund event per topup_id — twice only if a delivery is retried. Signing, retries and the 10-second timeout are the same as for esim.issued. You receive refund events only for requests you open through the API. A refund arranged with support by email is confirmed by email.

Verifying the signature

Every webhook carries two signature headers, alongside X-Esim-Story-Event: The signed string is the timestamp, a full stop, and the raw request body:
Compute HMAC-SHA256 over that string using your webhook signing secret, hex-encode it, and compare against the header.
The webhook signing secret is not your secret key. The secret key signs the requests you send us; the webhook signing secret verifies the webhooks we send you. Either can be replaced without affecting the other.
Sandbox and production have separate signing secrets, just as they have separate API keys. Your production secret is shown in your partner dashboard; your sandbox secret is issued with the rest of your sandbox credentials. The signing scheme is identical in both, so verification code written against sandbox works unchanged in production. Four details decide whether your implementation works: 1. Use the raw body, not a re-serialised object. Sign the bytes exactly as received. Parsing the JSON and serialising it again reorders keys and changes whitespace, so the signature will never match. Capture the raw body before any body-parsing middleware runs. 2. Use the secret verbatim. The signing secret is a plain string. It is not Base64 and must not be decoded first — unlike the secret key you sign requests with. 3. Compare in constant time. A plain == returns as soon as two characters differ, and that timing difference leaks the expected signature one character at a time. Use your language’s constant-time comparison. 4. Reject stale timestamps. The timestamp is inside the signed string, so a captured delivery cannot be replayed under a new one. Rejecting anything older than five minutes closes the remaining window.

Test vector

Check your implementation against these fixed values before going live.
The signature header is a list because it may one day carry more than one entry — during a secret changeover we would sign with both the outgoing and the incoming secret so neither side has downtime. Code that accepts a match against any entry today needs no change when that happens. Today exactly one signature is sent.

In sandbox

Sandbox delivers on the same clock production does: the 201 comes back first, and the webhook follows about five seconds later — one per unit, a second apart. Between the two, the order exists with its topup_id and no activation data yet. That gap is deliberate. A handler written against an instant webhook assumes the order response is already stored when the delivery lands, and that assumption breaks the first time it meets a real provider. Use SANDBOX-SLOW to see what your own code does while an order is still pending. It is a prefix match, so SANDBOX-SLOW-001 and so on work too. In production the wait is not fixed — usually seconds, occasionally minutes. Treat “issued” as something the webhook tells you, never something to assume from the time elapsed. Refund decisions are made within seconds in sandbox — see Request a refund in sandbox for how to get a rejection.
Last modified on November 2, 2026