SIMWAY
Partners

Orders and the lifecycle

An order is the only thing on this API that spends money, and it is asynchronous for a reason worth understanding before you write the client. This page covers why, what the states mean, when money comes back, and then the three endpoints in full.

Why ordering is asynchronous

One provisioning run can take up to 92 seconds in the worst case — the supplier gets 30 seconds per attempt and there are three, plus a settle delay. A quantity-50 order drives its units one at a time. And a typical HTTP client gives up at 30 seconds and retries.

A retry on a purchase is a double charge. So the endpoint answers 202 Accepted the instant the money moves and the work is durably recorded, and the provisioning runs detached. In the normal case it is finished in about 5 seconds — but the contract is written for the bad case, because the bad case is what breaks integrations.

The states

StatusMeansTerminal
pendingThe row claimed its idempotency key and no money has moved. The debit, the statement row, the unit fan-out and the move to processing are one transaction, so pending means that transaction never committed. You will almost never see it.No
processingDebited, units exist, provisioning is running or waiting to be re-driven.No
completedEvery unit delivered a profile.Yes
partialSome units delivered, some did not. The delivered profiles stay sold; the rest of the money is back on your balance.Yes
failedNothing was delivered. Money returned, or never taken.Yes
cancelledReserved. Nothing in the system ever writes it, and there is no cancellation endpoint. It is accepted as a list filter and nothing more.Yes

The polling contract

The whole client
let order = await createOrder(body, idempotencyKey);   // 202

while (order.poll_after_ms !== null) {
  await sleep(order.poll_after_ms);
  order = await getOrder(order.id);
}

switch (order.status) {
  case 'completed': return deliver(order.esims);
  case 'partial':   return deliver(order.esims), refundCustomerFor(order);
  case 'failed':    return apologise(order.failure_message);
}
  • poll_after_ms is advice, not a limit. The enforced limit is the readrate class. It is published because the two available outcomes are “an integration that polls sensibly” and “one that polls forty times a second because nobody gave its author a number”.
  • 2 seconds is roughly one supplier round trip, so polling faster mostly returns the same answer.
  • There are no webhooks on this API — none, of any kind. Polling is the delivery mechanism and nothing else is planned that you should build around.

Partial delivery

Delivery is read from the profiles that exist, and from nothing else — never from an in-flight error flag. If some units delivered and the rest provably produced nothing, the order settles partial with failure_code: "supplier_error" and a message naming the counts.

The delivered profiles stay sold.They are already yours and cannot be un-bought. Only the undelivered units are refunded, at the unit price that order was sold at — not at today’s price.

Nothing delivered at all is failed, with the whole amount returned.

When the money comes back

A refund is written in the same transaction as the terminal status. So by the time you can read failed or partial, GET /balance already reflects it — there is no window where the status says failed and the money is still gone.

  • On a live order it is one refund row on your statement, guarded by a uniqueness constraint so a concurrent re-drive cannot double-refund.
  • The amount is not read and then written — it is derived from the same conditional update that moves each unit from paid to refunded, so a unit that delivered in the meantime is not paid for twice or refunded wrongly.
  • On a sandbox order it moves the sandbox float and writes no row, because the sandbox writes no statement rows at all.

How long failure takes to be reported

This is the number your own timeout policy should be built on. An order whose plan can never be provisioned is retried 5 times, every 2 minutes, so it takes roughly 8 minutes to report failed — and it reads processing the whole time.

MechanismValue
Typical end-to-end provisioning~5 seconds
Hard ceiling on one provisioning run92 seconds
Fulfilment attempts before an order is failed5
Gap between attempts2 minutes
So, worst case before failed is visible8 minutes
Reconciler sweepevery 60 seconds
Fulfilment lease per order5 minutes
Drain on shutdown2 minutes

Sandbox does not make you wait for this: the fixture plan that fails on purpose settles on the first pass, because a failure we were asked to produce will not succeed on the fourth try.

Why you never have to retry an accepted order

  • The run is detached and tracked, with a 2 minutes drain on deploy.
  • A sweep every 60 seconds re-drives anything stranded for more than two minutes.
  • A single 5 minutes lease per order makes re-driving safe — two workers cannot provision the same unit.
  • A re-drive commits at the stored price, so an order stranded across a repricing settles at what it was accepted at.

Endpoints

The idempotency contract applies to the first one and to nothing else on this API.

POST/orders

Place an order

Requires a keyRate class write

The money moves synchronously and the profiles do not. By the time you hold the 202, your balance has been debited, the statement row exists and the child orders have been created; provisioning then runs detached and you poll for it.

It answers 202 rather than waiting because one provisioning run can take up to 92 seconds, a quantity-N order drives its units one at a time, and a typical HTTP client gives up at 30 seconds and retries. A retry on a purchase is a double charge. Asynchronous is not a design flourish here; it is the only shape that is safe.

Idempotency-Key is required. There is no way to place an order without one.

Headers

NameTypeRules
Idempotency-Key *stringUnique to this order — a UUID, or your own booking id. Send the SAME value if you retry. Scoped per partner and spanning both modes.16–255 characters after trimming

Body

NameTypeRules
plan_id *stringMust begin pl_.≤ 64 characters
quantityintegerHow many profiles. Each is provisioned individually.1–50Default 1
referencestringYour own identifier. Never parsed by us, echoed on every read, and exact-match filterable on the order list. Trimmed; empty becomes null.≤ 255 characters
metadataobjectYour own blob, echoed back verbatim — its inner keys are NOT converted to snake_case like the rest of the response. Must be a JSON object; an array or a bare string is a 400.serialises to ≤ 4096 bytes of UTF-8

Example

Request
curl -X POST 'https://api.simway.pro/partner/v1/orders' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000' \
  -H 'Idempotency-Key: 8f3c2a1e-7b64-4d59-9e02-1c5a7f8b3d20' \
  -H 'Content-Type: application/json' \
  -d '{
  "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
  "quantity": 3,
  "reference": "BK-99182",
  "metadata": {
    "bookingId": "BK-99182",
    "travellerEmail": "[email protected]"
  }
}'
202 — Accepted — money moved, profiles pending
{
  "object": "order",
  "id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
  "mode": "live",
  "status": "processing",
  "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
  "quantity": 3,
  "unit_price_cents": 1752,
  "total_cents": 5256,
  "list_unit_cents": 1990,
  "discount_bps": 1195,
  "price_source": "tier:silver",
  "tier": "silver",
  "reference": "BK-99182",
  "metadata": {
    "bookingId": "BK-99182",
    "travellerEmail": "[email protected]"
  },
  "esims": [],
  "failure_code": null,
  "failure_message": null,
  "balance_after_cents": 43053,
  "poll_after_ms": 2000,
  "created_at": "2026-08-17T09:14:22Z",
  "completed_at": null
}
  • X-Request-IdQuote it to support. Present on every response, including 401s.
200 — Replay — you have sent this before
{
  "object": "order",
  "id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
  "mode": "live",
  "status": "completed",
  "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
  "quantity": 3,
  "unit_price_cents": 1752,
  "total_cents": 5256,
  "list_unit_cents": 1990,
  "discount_bps": 1195,
  "price_source": "tier:silver",
  "tier": "silver",
  "reference": "BK-99182",
  "metadata": {
    "bookingId": "BK-99182",
    "travellerEmail": "[email protected]"
  },
  "esims": [
    {
      "object": "esim",
      "iccid": "8944500102030405062",
      "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
      "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "status": "provisioned",
      "activation": {
        "smdp_address": "rsp.example.com",
        "matching_id": "K2-1A9QX-88ZLM",
        "activation_code": "LPA:1$rsp.example.com$K2-1A9QX-88ZLM",
        "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
        "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-1A9QX-88ZLM"
      },
      "usage": {
        "data_used_bytes": 0,
        "data_total_bytes": 5368709120,
        "voice_used_min": 0,
        "voice_total_min": null
      },
      "activated_at": null,
      "expires_at": "2026-09-17T06:41:03Z"
    },
    {
      "object": "esim",
      "iccid": "8944500102030405070",
      "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
      "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "status": "provisioned",
      "activation": {
        "smdp_address": "rsp.example.com",
        "matching_id": "K2-7F4RD-31TVP",
        "activation_code": "LPA:1$rsp.example.com$K2-7F4RD-31TVP",
        "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
        "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-7F4RD-31TVP"
      },
      "usage": {
        "data_used_bytes": 0,
        "data_total_bytes": 5368709120,
        "voice_used_min": 0,
        "voice_total_min": null
      },
      "activated_at": null,
      "expires_at": "2026-09-17T06:41:03Z"
    },
    {
      "object": "esim",
      "iccid": "8944500102030405088",
      "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
      "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "status": "provisioned",
      "activation": {
        "smdp_address": "rsp.example.com",
        "matching_id": "K2-5B2WH-96YKS",
        "activation_code": "LPA:1$rsp.example.com$K2-5B2WH-96YKS",
        "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
        "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-5B2WH-96YKS"
      },
      "usage": {
        "data_used_bytes": 0,
        "data_total_bytes": 5368709120,
        "voice_used_min": 0,
        "voice_total_min": null
      },
      "activated_at": null,
      "expires_at": "2026-09-17T06:41:03Z"
    }
  ],
  "failure_code": null,
  "failure_message": null,
  "balance_after_cents": 43053,
  "poll_after_ms": null,
  "created_at": "2026-08-17T09:14:22Z",
  "completed_at": "2026-08-17T09:14:27Z"
}
  • Idempotency-Replayedtrue. Set on replays only.

Errors it can return

GET/orders

List orders

Requires a keyRate class read

Scoped to your account AND the mode of the key you sent. A live key never sees sandbox orders and vice versa.

Query parameters

NameTypeRules
statusenumFilter by state.One of: pending, processing, completed, partial, failed, cancelled
created_afterISO 8601EXCLUSIVE — >. Note this differs from /ledger, where it is inclusive.
created_beforeISO 8601Exclusive — <.
referencestringExact match on the reference you sent. Not a prefix and not a search.≤ 255 characters
limitintegerPage size.1–200Default 50
cursorstringOpaque. Pass back next_cursor from the previous page, or omit it to start again. A malformed cursor is a 400 naming cursor, never a silent first page.≤ 512 characters

Example

Request
curl 'https://api.simway.pro/partner/v1/orders?status=processing' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200 — One page
{
  "data": [
    {
      "object": "order",
      "id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
      "mode": "live",
      "status": "completed",
      "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "quantity": 3,
      "unit_price_cents": 1752,
      "total_cents": 5256,
      "list_unit_cents": 1990,
      "discount_bps": 1195,
      "price_source": "tier:silver",
      "tier": "silver",
      "reference": "BK-99182",
      "metadata": {},
      "esims": [
        {
          "object": "esim",
          "iccid": "8944500102030405062",
          "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
          "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
          "status": "provisioned",
          "activation": {
            "smdp_address": "rsp.example.com",
            "matching_id": "K2-1A9QX-88ZLM",
            "activation_code": "LPA:1$rsp.example.com$K2-1A9QX-88ZLM",
            "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
            "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-1A9QX-88ZLM"
          },
          "usage": {
            "data_used_bytes": 0,
            "data_total_bytes": 5368709120,
            "voice_used_min": 0,
            "voice_total_min": null
          },
          "activated_at": null,
          "expires_at": "2026-09-17T06:41:03Z"
        },
        {
          "object": "esim",
          "iccid": "8944500102030405070",
          "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
          "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
          "status": "provisioned",
          "activation": {
            "smdp_address": "rsp.example.com",
            "matching_id": "K2-7F4RD-31TVP",
            "activation_code": "LPA:1$rsp.example.com$K2-7F4RD-31TVP",
            "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
            "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-7F4RD-31TVP"
          },
          "usage": {
            "data_used_bytes": 0,
            "data_total_bytes": 5368709120,
            "voice_used_min": 0,
            "voice_total_min": null
          },
          "activated_at": null,
          "expires_at": "2026-09-17T06:41:03Z"
        },
        {
          "object": "esim",
          "iccid": "8944500102030405088",
          "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
          "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
          "status": "provisioned",
          "activation": {
            "smdp_address": "rsp.example.com",
            "matching_id": "K2-5B2WH-96YKS",
            "activation_code": "LPA:1$rsp.example.com$K2-5B2WH-96YKS",
            "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
            "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-5B2WH-96YKS"
          },
          "usage": {
            "data_used_bytes": 0,
            "data_total_bytes": 5368709120,
            "voice_used_min": 0,
            "voice_total_min": null
          },
          "activated_at": null,
          "expires_at": "2026-09-17T06:41:03Z"
        }
      ],
      "failure_code": null,
      "failure_message": null,
      "balance_after_cents": 43053,
      "poll_after_ms": null,
      "created_at": "2026-08-17T09:14:22Z",
      "completed_at": "2026-08-17T09:14:27Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Errors it can return

GET/orders/{id}

Get an order (the poll)

Requires a keyRate class read

This is the delivery mechanism. There are no webhooks on this API, so this is how you learn that an order finished.

The loop is: sleep poll_after_ms, read, repeat while poll_after_ms is not null. Do not enumerate terminal statuses yourself — the API decides what terminal means, and it treats an unrecognised status as terminal so a status added later cannot strand your loop.

Path parameters

NameTypeRules
id *stringMust begin po_.

Example

Request
curl 'https://api.simway.pro/partner/v1/orders/po_c7m2k9x4v1b6n3q8w5t0r2j7' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200 — Partially delivered, and settled
{
  "object": "order",
  "id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
  "mode": "live",
  "status": "partial",
  "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
  "quantity": 3,
  "unit_price_cents": 1752,
  "total_cents": 5256,
  "list_unit_cents": 1990,
  "discount_bps": 1195,
  "price_source": "tier:silver",
  "tier": "silver",
  "reference": "BK-99182",
  "metadata": {},
  "esims": [
    {
      "object": "esim",
      "iccid": "8944500102030405062",
      "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
      "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "status": "provisioned",
      "activation": {
        "smdp_address": "rsp.example.com",
        "matching_id": "K2-1A9QX-88ZLM",
        "activation_code": "LPA:1$rsp.example.com$K2-1A9QX-88ZLM",
        "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
        "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-1A9QX-88ZLM"
      },
      "usage": {
        "data_used_bytes": 0,
        "data_total_bytes": 5368709120,
        "voice_used_min": 0,
        "voice_total_min": null
      },
      "activated_at": null,
      "expires_at": "2026-09-17T06:41:03Z"
    },
    {
      "object": "esim",
      "iccid": "8944500102030405070",
      "order_id": "po_c7m2k9x4v1b6n3q8w5t0r2j7",
      "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "status": "provisioned",
      "activation": {
        "smdp_address": "rsp.example.com",
        "matching_id": "K2-7F4RD-31TVP",
        "activation_code": "LPA:1$rsp.example.com$K2-7F4RD-31TVP",
        "qr_code_data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
        "apple_universal_link": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.com%24K2-7F4RD-31TVP"
      },
      "usage": {
        "data_used_bytes": 0,
        "data_total_bytes": 5368709120,
        "voice_used_min": 0,
        "voice_total_min": null
      },
      "activated_at": null,
      "expires_at": "2026-09-17T06:41:03Z"
    }
  ],
  "failure_code": "supplier_error",
  "failure_message": "2 of 3 profiles were delivered; the other 1 could not be provisioned and have been returned to your balance.",
  "balance_after_cents": 44805,
  "poll_after_ms": null,
  "created_at": "2026-08-17T09:14:22Z",
  "completed_at": "2026-08-17T09:22:40Z"
}

Errors it can return