SIMWAY
Partners

Quickstart

Six requests take you from an empty terminal to an eSIM profile a phone can install. Everything below runs against sandbox, where the money is imaginary and nothing reaches a supplier — the base URL, the routes and the payloads are identical to live.

Before you start

  • A sandbox key. If you already have a SimWay partner account, mint one yourself in the cabinet — both roles may issue sandbox keys, and the secret is shown once. If you do not, email [email protected]: there is no signup form, because opening an account is a decision a person makes and a form in front of it only hides the wait.
  • A server. This API emits no CORS headers at all, deliberately, so a key cannot be used from browser JavaScript. Every call below belongs on your backend.
  • Nothing else. No SDK, no signing, no OAuth exchange. A bearer header and an HTTP client.

The base URL is https://api.simway.pro/partner/v1. It sits outside the consumer api/v1 prefix so that a v2 can ship one day without disturbing anything.

0. Check we are up

The one route that takes no credential and is subject to no rate limit. Point your monitoring at it — it answers 503 rather than a cheerful 200 when the database is unreachable, so an uptime check configured with nothing but a URL does the right thing.

Request
curl 'https://api.simway.pro/partner/v1/health'
200
{
  "status": "ok",
  "uptime_seconds": 41233,
  "database": "ok"
}

1. Confirm the key

Authentication is Authorization: Bearer <key> and nothing else. A key looks like this, and its shape is fixed so your own configuration check can be exact:

Key format
simway_sk_live_<43 base62 characters>   // live
simway_sk_test_<43 base62 characters>   // sandbox — note "test", not "sandbox"

^simway_sk_(live|test)_[0-9A-Za-z]{43}$

Example used throughout these docs (fake, but the right shape):
simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000

GET /me is the fastest way to prove a credential works and to see what it is allowed to do. Read rate_limits from here rather than hard-coding the table from the rate-limits page: it moves with your tier without us redeploying anything.

Request
curl 'https://api.simway.pro/partner/v1/me' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200
{
  "object": "partner",
  "id": "pt_c8k2m4p9x1v7b3n6q0w5t2r8",
  "name": "Northwind Travel",
  "slug": "northwind-travel",
  "status": "live",
  "mode": "live",
  "tier": "silver",
  "key": {
    "id": "pk_c4h9j2n7x5v1b8m3q6w0t4r2",
    "label": "production-checkout",
    "last4": "aB3x",
    "mode": "live",
    "expires_at": null,
    "created_at": "2026-07-02T11:20:41Z"
  },
  "rate_limits": {
    "read": {
      "limit": 20,
      "window_seconds": 1
    },
    "write": {
      "limit": 25,
      "window_seconds": 5
    },
    "supplier": {
      "limit": 2,
      "window_seconds": 1
    },
    "catalogue": {
      "limit": 120,
      "window_seconds": 60
    }
  },
  "created_at": "2026-06-18T08:02:15Z"
}

2. Find something to sell

GET /plans is the whole catalogue, priced for your account. Filter it down — an unknown query parameter is a 400 rather than an ignore, so a typo tells you instead of quietly returning everything.

Request
curl 'https://api.simway.pro/partner/v1/plans?country=ZZ' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200
{
  "data": [
    {
      "object": "plan",
      "id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
      "name": "Example Republic 5 GB / 30 days",
      "country_code": "ZZ",
      "region": null,
      "global": false,
      "plan_type": "fixed",
      "data_amount_mb": 5120,
      "daily_allowance_mb": null,
      "fup_speed": null,
      "unlimited": false,
      "duration_days": 30,
      "voice_minutes": null,
      "sms_count": null,
      "coverage_types": [
        "data"
      ],
      "speed": "4G/LTE",
      "networks": [
        "Example Mobile",
        "Example Telecom"
      ],
      "requires_kyc": null,
      "list_price_cents": 1990,
      "your_price_cents": 1752,
      "discount_bps": 1200,
      "price_source": "tier:silver",
      "tier": "silver",
      "min_resale_price_cents": null,
      "available": true
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiWloiLCJkIjo1MTIwLCJ0IjozMCwiaSI6ImMzbjhrNXgydjliNG03cTF3NnQzcjBqNSJ9"
}

Three prices travel together on every row and the third is the one that matters: list_price_cents is what a consumer pays us, your_price_cents is what you pay, and discount_bps is the rate that price actually represents on that plan — which is not always your headline tier rate. The pricing page explains when and why.

Pages default to 50 rows and cap at 200. Follow next_cursor until has_more is false. There is no total count anywhere on this API.

3. Price it before you buy it

A dry run. It deducts nothing, provisions nothing and reserves nothing, and it tells you whether the balance would clear. Calling it before every order is the cheapest habit on this API, because the alternative — discovering the balance is short at order time — costs you an idempotency key permanently.

Request
curl 'https://api.simway.pro/partner/v1/plans/pl_c3n8k5x2v9b4m7q1w6t3r0j5/price?quantity=3' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200
{
  "plan_id": "pl_c3n8k5x2v9b4m7q1w6t3r0j5",
  "quantity": 3,
  "unit_price_cents": 1752,
  "total_cents": 5256,
  "list_unit_cents": 1990,
  "discount_bps": 1200,
  "price_source": "tier:silver",
  "tier": "silver",
  "balance_cents": 48309,
  "sufficient_balance": true,
  "mode": "live"
}

4. Order

The only endpoint that spends money, and the only one that requires an Idempotency-Key. The money moves synchronously and the profiles do not: by the time you hold the response, your balance is debited and the statement row exists, and provisioning is running in the background.

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
{
  "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
}

5. Poll until it is done

There are no webhooks on this API. Polling is the delivery mechanism, and the order tells you the interval so you do not have to invent one.

The loop
let order = await createOrder();          // 202

while (order.poll_after_ms !== null) {
  await sleep(order.poll_after_ms);       // 2 seconds while in flight
  order = await getOrder(order.id);
}

// poll_after_ms is null: the order is terminal.
// Read order.status and order.esims — do not enumerate statuses yourself.
Request
curl 'https://api.simway.pro/partner/v1/orders/po_c7m2k9x4v1b6n3q8w5t0r2j7' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'

Typical end to end is about 5 seconds. The hard ceiling on one provisioning run is 92 seconds, which is why the endpoint answers 202 rather than making you wait — and why an order that will never provision takes a few minutes to say so. The lifecycle, including partial delivery and refunds.

6. Deliver the profile

There is nothing left to fetch: each entry in esims[] from step 5 is already the whole profile, install payload and all — the object below is what you are holding. Read it again by ICCID when your own support process needs it later, which is the identifier a customer writing in will have.

Request
curl 'https://api.simway.pro/partner/v1/esims/8944500102030405062' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200
{
  "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"
}

The activation object holds every route onto a handset: a scannable QR as a data URL, an LPA: activation code, the SM-DP+ address and matching id for manual entry, and an Apple universal link that installs the profile in one tap on iOS. What each field is for.

Conventions worth reading once

ConventionDetail
CaseRequests and responses are snake_case throughout. Your own metadata blob is the exception — it is echoed back verbatim, inner keys untouched.
TimesRFC 3339 UTC to the second, with a Z suffix: 2026-08-17T09:14:22Z. Never a local time and never a bare offset.
MoneyInteger cents, USD, always. There is no other billing currency and no float anywhere in a price.
Absent valuesA field that is null means null. A field that has no value is omitted from the response entirely rather than sent as null.
Byte countersdata_used_bytes and data_total_bytes are JSON numbers on this API. They are strings on the consumer API — if you have integrated both, they are not the same type.
Unknown fieldsRefused, not ignored. An unrecognised query parameter or body field is a 400 naming it.
Lists{ data, has_more, next_cursor }. Default 50, maximum 200, no total count anywhere.
Request idsEvery response carries X-Request-Id, including 401s. Send your own and we echo it back if it is 8–64 characters of [A-Za-z0-9_-].

Id prefixes

Every id names its own kind, and an id of the wrong kind is refused as a 400 naming the parameter rather than 404’d — a client bug that says what it is instead of looking like a missing record.

PrefixNames
pt_a partner account
po_a partner order
pl_a plan
pk_an API key
led_a ledger entry
dep_a deposit
req_one request (echoed as X-Request-Id)

The whole surface, at a glance

Sixteen routes. Fifteen need a key; one does not.

EndpointWhat it does
GET/healthLiveness, without a credential.
GET/meWho this credential belongs to, and what it may do.
GET/balancePrepaid funds, and the headline discount they buy.
GET/ledgerEvery movement of money, newest first.
POST/depositsMint a crypto invoice. It credits the float when it settles.
GET/depositsYour funding history, newest first — and where to pay what is open.
GET/deposits/{id}One deposit, including where to pay it.
GET/plansThe whole catalogue, priced for you.
GET/plans/{id}Resolve a stored plan id, including a delisted one.
GET/plans/{id}/priceWhat an order would cost, and whether it would clear.
GET/destinationsEvery country you can sell, with your own entry price.
POST/ordersThe only endpoint that spends money. Always answers 202.
GET/ordersYour orders, newest first, scoped to this key’s mode.
GET/orders/{id}Where an accepted order actually ends up.
GET/esimsEvery profile you have bought.
GET/esims/{iccid}Everything needed to install it, and how much is left.

Where to go next

Authentication
Bearer keys, the live and sandbox modes, how a key is stored and rotated, IP allowlists and exactly what they are worth.
Idempotency
The Idempotency-Key contract, the four things a replay can do, and the rule that a 402 binds its key permanently.
Orders and the lifecycle
Why ordering answers 202, the polling loop, what partial delivery means, when refunded money lands, and the three order endpoints in full.
Catalogue
Listing plans, resolving a stored plan id, the price dry run and the destination list — with the incremental-sync caveat.
eSIM profiles
The install payload, what each activation field is for, usage counters and how stale they can be.
Account, balance and statement
Health, identity, prepaid balance, the statement and the deposit rail — including what happens when a payment arrives over, under, or after the account was closed.
Errors
Every error code, its status, what caused it and what to do. This page is what the doc_url in every error body points at.
Rate limits
Four rate classes, the limits per tier, the response headers to pace yourself with, and the two different 429s.
Sandbox
A real environment with a real balance, four fixture plans that force the failures you cannot otherwise rehearse, and the ten ways it differs from live.
Going live
What approval involves, how a live key is issued, how deposits work today, and the checklist worth passing before you switch.
Changelog
Every change to the contract, newest first, and the rule for which parts of a response are safe to depend on.

Why these pages are English only

The rest of simway.pro is published in eight languages. This section is not, and the reason is worth stating rather than leaving as an oversight.

  1. The site’s translation files are typed against the English one, so exactly the same key set is required in all eight. A reference this size would be thousands of translated strings before the first deploy, and it would break the build again every time an endpoint gained a parameter.
  2. The content resists translation where it counts. Field names, enum values, error codes, HTTP semantics and JSON bodies stay English whatever the surrounding prose says — a translated page would be English payload inside translated wrapping, which is harder to keep truthful and worse to read than the English it replaced.
  3. One URL per page also means no hreflang set to get wrong. Declaring eight translations that do not exist is a reliable way to have the whole set discarded, including the ones on the consumer catalogue that do real work.

If you would rather read this as one flat document — or hand it to a coding assistant — the whole reference is at /partners/llms-full.txt, generated from the same source as these pages.