SIMWAY
Partners

Catalogue

Four endpoints over 4,090 active plans in 190 destinations, every one of them priced for your account at read time. Nothing about your price is stored on a plan row, which is why you must read it rather than compute it.

How to think about it

  • A plan is one bundle in one place. Single-country plans carry a country_code; regional and global bundles carry none and a region or global: true instead.
  • Prices are computed per request, per account. There is deliberately no stored partner price on a plan row: the catalogue sync rewrites plan rows wholesale every 30 minutes, and a stored agreed price would be silently overwritten twice an hour.
  • Three prices travel together. 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. When they differ, and why.
  • Listing is its own rate class. GET /plans is the only route in the catalogue class — 60 requests a minute at the bottom tier — because one request is the whole price list computed for one buyer.

Mirroring the catalogue

Most integrations keep a local copy. Two endpoints do the work and one caveat decides the shape of your sync job.

Incremental
# Everything that changed since your last successful pass.
GET /plans?updated_since=2026-08-17T09:00:00Z&limit=200
# then follow next_cursor until has_more is false.

Ordering across pages is stable — country, then data, then duration, then id — and the cursor is a keyset over exactly that tuple, so a full walk sees every row once even while the 30-minute sync rewrites rows underneath it. Do not construct cursors; pass back next_cursor verbatim.

The carrier data

Every plan can carry networks — the mobile operators it roams onto, each with its generation. We hold it for 187 of 190 destinations, naming 150 distinct operators. It is the one part of this catalogue that is not answerable from general knowledge, and it is the fact your own customers ask about.

  • networks comes from the supplier’s operator list and never from the speed field. Those two use different vocabularies and disagree with each other on more than half the catalogue. speed describes how the plan is sold; networks describes the agreement the traveller actually arrives on.
  • nullmeans we hold no operator data for that plan. It does not mean the plan roams onto nothing, and it must not be rendered as “no coverage”.
  • requires_kyc is null on every row today, and null means unknown — published as unknown on purpose rather than as false, which would be a claim.
  • min_resale_price_cents is null on every row. There is no minimum resale price today; the field is a forward-compatible slot.

When a plan cannot be sold to you

A plan can exist and still be un-buyable, in which case it comes back available: false on the catalogue and 422 plan.unavailable on the order path. There are two causes and they need different responses from you.

CauseWhat it meansWhat to do
Price floor above our own retailThat plan’s supplier cost has risen since it was last repriced, so your discounted price would sit above what a consumer pays us. Quoting you more than the public price is not an offer.Skip it. It is not your fault and not retryable on a timer.
Not pricedThe catalogue row has no usable price. This is a data fault on our side.Skip it, and quote the plan id to support.

The dry run says only that the plan is unavailable, because it holds the boolean and not the reason. Placing the order tells you which.

The destination list

GET /destinations is built for the page you put in front of your own customers. Two things about it are worth knowing before you render it: from_price_cents is the cheapest price you can buy in that destination rather than the cheapest list price, and regional or global bundles are excluded entirely because they carry no country code. It is cached for 10 minutes, keyed on your rate rather than your account, so partners on the same rate share one entry.

Endpoints

GET/plans

List plans

Requires a keyRate class catalogue

This is the only endpoint in the catalogue rate class, and it is the reason that class exists: one request is the entire price list computed for one buyer.

Ordering is stable across pages — country code, then data, then duration, then id — and the cursor is a keyset over exactly that tuple, so a full walk sees every row once even while the catalogue sync rewrites rows underneath it.

Query parameters

NameTypeRules
countrystringISO 3166-1 alpha-2, case-insensitive. The examples here use ZZ, a code ISO reserves for private use so that nothing in this reference can be mistaken for a claim about a real destination — send a real one (JP, TR) against your own key.at most 2 characters — a 3-letter code is a 400, but a 1-letter code validates and returns an empty page rather than an error
regionstringMatches either a regional-bundle slug or a country’s region.≤ 64 characters
typeenumBundle shape.One of: fixed, daily
min_daysintegerInclusive lower bound on duration_days.≥ 1
max_daysintegerInclusive upper bound on duration_days.≥ 1
min_data_mbintegerInclusive lower bound on data_amount_mb.≥ 0
max_data_mbintegerInclusive upper bound on data_amount_mb.≥ 0
unlimitedenumThe strings only. ?unlimited=yes is a 400, deliberately.One of: true, false
updated_sinceISO 8601EXCLUSIVE — >. The incremental-sync parameter.
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/plans?country=ZZ' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200 — One page of plans
{
  "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"
}

Errors it can return

GET/plans/{id}

Get one plan

Requires a keyRate class read

The important divergence from the list: an INACTIVE plan is returned here with available: false rather than 404’d. That is what lets a mirror resolve an id it stored last week and propagate the delisting to its own catalogue.

A plan that never existed, that is a sandbox fixture seen by a live key, or that sits in a withdrawn destination is genuinely a 404.

Path parameters

NameTypeRules
id *stringMust begin pl_. A wrong-kind id is a 400 naming id, not a 404.

Example

Request
curl 'https://api.simway.pro/partner/v1/plans/pl_c3n8k5x2v9b4m7q1w6t3r0j5' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200 — A delisted plan
{
  "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": false
}

Errors it can return

GET/plans/{id}/price

Price a purchase (dry run)

Requires a keyRate class read

Deducts nothing, provisions nothing, reserves nothing. Call it before every order and you never spend an idempotency key on a 402 — which matters here more than on most APIs, because a 402 binds its key permanently.

The quantity cap matches the order endpoint exactly, so a basket that prices cannot fail to order for want of a bigger cap.

Path parameters

NameTypeRules
id *stringMust begin pl_.

Query parameters

NameTypeRules
quantityintegerHow many profiles.1–50Default 1

Example

Request
curl 'https://api.simway.pro/partner/v1/plans/pl_c3n8k5x2v9b4m7q1w6t3r0j5/price?quantity=3' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200 — A quote
{
  "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"
}

Errors it can return

GET/destinations

List destinations

Requires a keyRate class read

Built for the page a partner puts in front of their own customers: a country, a plan count, and a price to say “from”.

from_price_cents is the cheapest price YOU can buy in that destination, not the cheapest list price. The two differ, so quoting the list minimum on your own site would advertise a margin you do not have.

Example

Request
curl 'https://api.simway.pro/partner/v1/destinations' \
  -H 'Authorization: Bearer simway_sk_test_ExampleKeyDoNotUse0000000000000000000000000'
200 — Destinations
{
  "data": [
    {
      "code": "ZZ",
      "name": "Example Republic",
      "region": "asia",
      "plan_count": 34,
      "from_price_cents": 1752
    },
    {
      "code": "XA",
      "name": "Example Islands",
      "region": "europe",
      "plan_count": 28,
      "from_price_cents": 872
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Errors it can return