Create Order
Purchase an eSIM and receive its activation code (LPA string) and ICCID
POST /api/v1/esim/orders
Purchases an eSIM. Octopus Cards picks the matching variant for (product_id, amount), debits your wallet, dispatches the order for fulfilment, and returns the activation code (an LPA string the device parses) along with the ICCID.
Hybrid sync/async. Every response is 200 OK — branch on the status field in the body:
- If fulfilment completes inside the 60-second handler deadline →
status: DELIVEREDwith the full activation payload (activation_code,iccid). - If 60 seconds elapse →
status: PENDING. A detached goroutine keeps trying in the background; pollGET /esim/orders/:iduntil terminal. - Synchronous user-fixable failures (invalid plan, etc.) come back as
status: FAILEDwithfailure_code/failure_reason/is_user_fixablepopulated. Wallet auto-refunded.
Quantity is fixed at 1 — each order produces exactly one eSIM. Send quantity: 1 or omit it; any other value is silently clamped.
Request
curl -X POST "https://api.octopuscards.io/api/v1/esim/orders" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"product_id": 712,
"amount": 4.50,
"quantity": 1,
"client_reference": "TRIP_TOKYO_OCT"
}'Request Parameters
| Key | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | eSIM product to purchase. Must be > 0. |
amount | number | Yes | Must match an existing variant's amount for this product. Use this as the variant selector — Octopus Cards resolves (product_id, amount) to a single variant. |
quantity | integer | No | Always coerced to 1. eSIM orders are one-eSIM-per-order. |
client_reference | string | No | Your dedup + lookup key (printable ASCII, max 255 characters). Unique per client — a duplicate (client_id, client_reference) returns 400 "Duplicate client_reference". Optional, but required for retry-safe order creation. See Idempotency. |
wallet_id | integer | No | Optional. The wallet to debit. When supplied and its currency differs from the variant's, Octopus Cards looks up an admin-managed forex rate and bills the wallet in its own currency. If no rate exists for the pair, the request rejects with 400 "Exchange rate not available for the wallet currency". Omit to use the variant-currency wallet, falling back to your default-currency wallet. |
category | string | No | Reserved for future variant categorisation. Leave empty unless instructed. |
redeem_voucher_code | string | No | Fund the order by redeeming a previously-delivered voucher code instead of debiting the wallet. The voucher must be in (DELIVERED, COMPLETED) and unredeemed. |
Response (fulfilled in time — status: DELIVERED)
200 OK
{
"id": 9302481,
"client_reference": "TRIP_TOKYO_OCT",
"status": "DELIVERED",
"status_text": "eSIM ready to install",
"product_name": "Japan eSIM",
"country_code": "JPN",
"data_amount_gb": 1.0,
"validity_days": 7,
"amount": 4.275,
"currency": "USD",
"activation_code": "LPA:1$rsp.example.com$ACTIVATION-TOKEN-XYZ",
"iccid": "8910300000123456789",
"activation_status": "NOT_INSTALLED",
"created_at": "2026-05-15T14:30:00Z"
}The activation_code is the LPA Activation Code — the string the customer scans (via QR) or pastes into their device's eSIM setup screen. Once installed, the activation_code field is masked from subsequent responses (see Lifecycle).
Response (still in flight after 60 seconds — status: PENDING)
200 OK
{
"id": 9302482,
"client_reference": "TRIP_TOKYO_OCT",
"status": "PENDING",
"status_text": "Awaiting fulfilment",
"product_name": "Japan eSIM",
"country_code": "JPN",
"data_amount_gb": 1.0,
"validity_days": 7,
"amount": 4.275,
"currency": "USD",
"activation_status": "NOT_INSTALLED",
"created_at": "2026-05-15T14:30:00Z"
}Poll GET /esim/orders/:id — the activation code lands once fulfilment completes. A detached dispatch goroutine keeps trying in parallel; the retry cron handles any remaining slow cases.
Response (rejected with user-fixable failure — status: FAILED)
200 OK
{
"id": 9302483,
"status": "FAILED",
"status_text": "Plan unavailable in this region",
"amount": 4.275,
"currency": "USD",
"activation_status": "NOT_INSTALLED",
"failure_code": "PRODUCT_UNAVAILABLE",
"failure_reason": "This plan is not currently available for new purchases. Try a different plan.",
"is_user_fixable": true,
"created_at": "2026-05-15T14:30:00Z"
}The wallet is automatically refunded for failed orders.
Enumerations
status:
| Value | Meaning |
|---|---|
PENDING | Order created, fulfilment in flight. Returned when the 60s handler deadline fires without fulfilment landing. Not terminal — poll the detail endpoint. |
DELIVERED | Activation code issued. Terminal for the order; the eSIM may still need to be installed (see activation_status). |
FAILED | Order could not be fulfilled. Wallet auto-refunded. Terminal. |
CANCELLED | Order cancelled administratively. Wallet refunded. Terminal. |
activation_status (lifecycle on the device; separate from order status):
| Value | Meaning |
|---|---|
NOT_INSTALLED | Activation code returned but not yet installed on a device. |
ACTIVE | eSIM installed and currently active on a device. |
EXPIRED | Validity window has passed. |
Response Fields
| Key | Type | Description |
|---|---|---|
id | integer | Server-issued order ID. Pass to /esim/orders/:id. |
client_reference | string | Echo of the value you supplied on create (omitted if you didn't supply one). |
status | string | Order status (see enum). |
status_text | string | Human-readable status detail. |
product_name | string | Display name of the product. |
country_code | string | ISO 3166-1 alpha-3 code (or GLO). |
data_amount_gb | number | Included data in GB. |
validity_days | integer | Plan validity in days, from activation. |
amount | number | Wallet debit amount (after client discount). |
currency | string | ISO 4217. |
activation_code | string | LPA string. Only present when delivered AND not yet installed. The customer scans this (as a QR) or pastes it into their device's eSIM setup. |
iccid | string | SIM serial number. Use this to look up installation status later via the lifecycle endpoints. |
activation_status | string | One of NOT_INSTALLED, ACTIVE, EXPIRED. |
is_installed | boolean | true once the device has installed the eSIM. The activation code is masked from then on. |
installed_at | string | RFC 3339 timestamp of installation. Present once is_installed is true. |
activated_at | string | RFC 3339 timestamp of activation (first data usage). |
failure_code | string | Canonical machine code when status == FAILED. Shares the Failure Codes enum with topups — same routing logic applies. |
failure_reason | string | User-facing failure message when status == FAILED. |
is_user_fixable | boolean | true when the customer can retry with different inputs. |
transaction_id | integer | Wallet transaction ID after debit. |
created_at | string | RFC 3339. |
HTTP Status Summary
| Status | When | Body |
|---|---|---|
200 OK | Order accepted. Branch on status in the body: DELIVERED (with activation code), PENDING (60s deadline elapsed — poll for terminal state), or FAILED (synchronous user-fixable failure, wallet refunded). | |
400 | Validation, balance, voucher errors | error envelope. |
404 | No matching variant or wallet | error envelope. |
401 / 403 / 500 | Auth / IP / server | error envelope. |
Errors
400 Bad Request — required fields missing or invalid.
{
"error": {
"name": "ValidationException",
"code": "VALIDATION_FAILURE",
"message": "Product ID is required"
}
}Other messages: "Amount is required", "Invalid request body".