Skip to main content
POST

Authentication

This endpoint requires HMAC-SHA256 signature authentication. See Authentication guide for details.
Two things to get right, in the order they bite:
  1. The signature covers a SHA-256 of the request body. Serialise the payload to a string once, hash that string, and send that same string — re-serialising for the request changes the bytes and the signature fails.
  2. The path in the signature is the full request path including the /api prefix/api/v1/api_partner/orders, not /v1/api_partner/orders.
The playground below cannot sign for you. It sends whatever you type, and this endpoint rejects anything unsigned — so filling in a body and pressing send returns 401. That is the endpoint working, not a fault.To use it, generate the signature elsewhere and paste it in:
  1. Build the signature over the exact body string you are about to send — copy it out of the playground first, or paste a body you have already hashed.
  2. Paste the result into X-Esim-Story-Signature, with the same timestamp you signed with in X-Esim-Story-Timestamp.
  3. Send within 5 minutes of that timestamp, and do not edit the body afterwards — either one invalidates the signature.
For checking your own implementation, the test vector is the faster path: it fixes the credentials, timestamp and body, and states the signature they must produce.

Headers

string
required
Your partner access key. Used for authentication.
string
required
HMAC-SHA256 signature of the request. Generated using your secret key. See Authentication guide for signature generation details.
string
required
Unix timestamp in seconds (UTC). Must be within 5 minutes of server time, in either direction.

Request body

string
required
Your order identifier in your system. Must be unique within your partner account. Re-sending the same value is safe — see Idempotency.
array
required
One entry per product you want to order.
One order carries at most 100 eSIMs in total, counted across every entry — there is no separate limit on how many entries or destinations you combine. Split a larger batch across several orders, each with its own external_order_id. Contact support if your volume needs a higher ceiling.
option_id values are not discoverable through the API. Your product catalogue is issued to you as a CSV when your partner account is set up. Contact support if you need an updated one.

Reading your catalogue

The CSV carries one row per orderable product, with these columns: Only option_id belongs in the payload. Do not send the other columns — duration, data allowance, and country are already fixed by the option_id you choose, and there is no field to override them. wholesale_price_usd is your contracted price for that product. It is specific to your account, so it is not comparable with another partner’s catalogue. option_id is matched case-insensitively, so send it exactly as your CSV spells it. There is no need to change its case to match the examples on this page.

Response

A successful request returns 201 Created. 201 means the order was accepted and provisioning was requested. It does not contain the eSIM. That arrives later, one delivery per unit, on your webhook.
array
required
One entry per unit ordered — a product with qty: 2 produces two entries.

Webhooks

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. 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: topup_id is unique per unit and never reused, so it is the natural deduplication key.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.

Payload

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

Verifying the signature

Every webhook carries two extra headers: 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.

Idempotency

external_order_id is the idempotency key. It is unique per partner account, and re-sending a request with an external_order_id you have already used is safe:
  • If the order exists but no eSIMs were issued yet (a previous attempt failed during provisioning), provisioning is retried and no duplicate order items are created.
  • If the order was already fully provisioned, the original topup_id values are returned unchanged. No second eSIM is issued and you are not charged twice.
This makes 422 provisioning failures and 5xx responses safe to retry verbatim. Always retry with the same external_order_id — generating a new one will create a genuinely new order.
Idempotency is scoped to your partner account and to external_order_id alone. Re-sending the same external_order_id with a different products payload returns the original order’s items and silently ignores the new payload. Use a fresh external_order_id for a genuinely different order.

Errors

400 Bad Request

The request body could not be parsed as JSON. The error value is the raw parser message and its exact wording is not stable — do not match on it.
Common causes:
  • Malformed or truncated JSON in the request body
  • Missing or empty request body

401 Unauthorized

Authentication failed. The body is always this shape:
Invalid signature. is the one worth debugging carefully — the Authentication guide lists its causes in order of likelihood, and a test vector to check your signing code against.

422 Unprocessable Entity

Validation or provisioning errors. The body always has an errors array of strings. Invalid request payload — the order is rejected before anything is created, so nothing is charged and no order record exists:
A single invalid entry rejects the whole request — products are never partially ordered. Provisioning failed — the eSIM could not be issued. Four variants, told apart by their prefix. All four are retryable with the same external_order_id:
A 422 from provisioning means the order record was already created but no eSIM was issued. Retry with the same external_order_id — the retry is idempotent and will not create a duplicate order. See Idempotency. A 422 from payload validation creates nothing, so fix the payload and send it as a new request.

Testing provisioning failures in sandbox

Sandbox issues an eSIM for every order, so the 422 above never occurs there on its own. To exercise your error handling, start the external_order_id with one of these: It is a prefix match, so append your own suffix to keep each test order unique — SANDBOX-FAIL-001, SANDBOX-FAIL-002, and so on. These prefixes do nothing in production. An order sent there with a name beginning SANDBOX-FAIL is provisioned normally.

Webhook timing 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 — the same prefix rules apply, so SANDBOX-SLOW-001 and so on. 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.
The simulation reproduces the aftermath, not just the status code. As in production, the order and its items are created before provisioning is attempted — so a simulated failure leaves an order that exists with no topup_id, and retrying it with the same external_order_id succeeds and issues the eSIM. That fail-then-retry sequence is the part worth rehearsing: it is what a real provider outage looks like from your side.

429 Too Many Requests

Rate limit exceeded. Check the Retry-After response header (in seconds) before retrying. See Rate Limits.

500 Internal Server Error

Returned for unexpected server-side failures. The response body is not guaranteed to be JSON — do not parse it. Treat any 5xx as retryable with the same external_order_id.

Debugging by symptom

The tables above are indexed by status code. This one is indexed by what you actually see first, including the two failures that return no error at all.
Last modified on August 24, 2026