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.
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.
string
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.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 — withevent
in front, as on every webhook.
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, alongsideX-Esim-Story-Event:
The signed string is the timestamp, a full stop, and the raw request body:
HMAC-SHA256 over that string using your webhook signing secret, hex-encode it, and compare against the header.
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: the201 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.