# SIMAREA API — v1 reference > The complete SIMAREA HTTP API as Markdown: every route, every field with its exact type and nullability, every error code, and the rules that are not visible in the shapes. This file and the HTML documentation, the OpenAPI document and the Postman collection are all rendered from one description of the API, so none of them can disagree with another or with the routes. It is complete on its own: a correct client can be written from this file without ever loading the HTML page. If you are asking a model to write an integration, give it this file. - **Base URL** — `https://simarea.com/api/v1` - **Authentication** — `Authorization: Bearer sk_live_…` on every request - **HTML documentation** — https://simarea.io/reseller/api/ - **This file** — https://simarea.io/reseller/api/llms.txt - **Support** — support@simarea.com, quoting the `x-request-id` off the response ## Contents - **Introduction** — What the API sells and how it relates to the website. - **Quickstart** — From nothing to a bought number in four calls. - **Authentication** — Bearer keys, what they carry, and how they are refused. - **Response format** — The envelope, money, dates, ids, and what is never in a response. - **Idempotency** — Why every purchase needs a key, and what a reused one does. - **Pagination** — How to walk a long order history. - **Rate limits** — Per key, per minute, with the window on every response. - **Errors** — The full code table, what retryable means, and which codes carry details. - **OpenAPI, Postman and llms.txt** — Machine-readable descriptions of everything on this page. - **Response headers** — what every response carries, and which two are conditional. - **eSIM data plans** — Destinations, plans, buying an eSIM and handing over the QR code. - **Virtual numbers** — Rent a dedicated non-VoIP line by the term and read its messages. - **Account** — Your balance, and checking what a key can do. - **Data shapes** — every object that crosses the wire, field by field. - **Machine-readable descriptions** — the same API as OpenAPI 3.1 and as a Postman collection. ## Introduction Everything SIMAREA sells is reachable over HTTP with an API key: the catalog, your balance, buying an eSIM data plan or a phone number, reading the SMS that arrives on it, and canceling an order. There is one account and one prepaid USD balance behind both surfaces. An order placed through this API is the same order you see when you sign in to the website — same row, same balance, same refund rules. There is no separate reseller ledger to reconcile. - **eSIM data plans** — pick a destination, buy a plan, hand the customer a QR code to install. - **Virtual numbers** — rent a dedicated non-VoIP line by the term and keep its full message history. The two are separate product families and never collapse into one label. They differ in how long they last, what they cost, and how you buy them — so the reference below is organised by family, and each family section carries every call you need for that product start to finish. | Detail | Value | | --- | --- | | Base URL | `https://simarea.com/api/v1` | | Auth | `Authorization: Bearer sk_live_…` | | Content type | `application/json` | | Currency | USD, always, as integer cents in a string | | Versioning | The version is in the path, so a future `v2` cannot change what `v1` returns | ## Quickstart - Create a SIMAREA account and top up your balance. - Issue an API key from the **API keys** panel in your account. It is shown once, at issue time, and cannot be recovered — store it before you close the dialog. - Check the key works and see what it can do. - Read a price, then buy. ```bash # 1 — is my key alive, and what may it do? curl https://simarea.com/api/v1/ \ -H "Authorization: Bearer sk_live_..." # 2 — what does a US virtual number cost right now, and for which terms? curl "https://simarea.com/api/v1/catalog/offers/?family=virtual_number&country=usa" \ -H "Authorization: Bearer sk_live_..." # 3 — rent one. The Idempotency-Key is required. curl -X POST https://simarea.com/api/v1/orders/ \ -H "Authorization: Bearer sk_live_..." \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "family": "virtual_number", "countrySlug": "usa", "serviceSlug": null, "duration": "oneWeek" }' # 4 — poll the order and read the messages it receives. curl https://simarea.com/api/v1/orders/o_5rT8w1Nq3vHc/ \ -H "Authorization: Bearer sk_live_..." ``` > **eSIM plans are not purchasable yet.** > > That is why the purchase above is a virtual number. The eSIM section explains the reason, and what you can build and test in the meantime. ## Authentication Issue a key from the API keys panel in your account and send it as a bearer token on every request. ```bash curl https://simarea.com/api/v1/balance/ \ -H "Authorization: Bearer sk_live_..." ``` > **The header is the only way in.** > > There is no `?api_key=` query parameter and there will not be one. Query strings land in web-server access logs, in browser history, in `Referer` headers on outbound links, and in the error reports of every proxy in between. A key in a URL is a key that has leaked. A missing, unknown, revoked or expired key — and a key belonging to a suspended account — all answer **401** with the same code, deliberately: telling a caller which of those it was tells an attacker which of those it was. **Revocation takes effect on the very next request.** Nothing about a key is cached, anywhere, for any length of time. If you believe a key has leaked, revoke it and the next call made with it fails. Issue **one key per integration**. Rate limits are per key, so a burst in your bot cannot throttle your storefront, and revoking one does not take the others down with it. Each key carries a set of permissions. A key that lacks the one a route requires gets **403**, and nothing is charged. | Permission | What it allows | | --- | --- | | `read` | Every GET: the catalog, your balance, your order list, one order, and the API root. | | `order` | Create and cancel virtual number and eSIM orders — families `virtual_number` and `esim`. | | `admin` | A key-level wildcard implying the two above. It grants nothing beyond the routes on this page: there is no administrative surface on the public API. | ## Response format One thing comes back under a singular key, many under a plural one, and a failure under `error`. A response is never a bare JSON array, so a list can grow a sibling field — as `orders` did when it gained `nextCursor` — without breaking your client. ```json { "order": { ... } } one order { "orders": [ { ... } ], "nextCursor": null } a list { "error": { "code": "...", "message": "..." } } a failure ``` - **Money is a decimal string of integer cents**, never a JSON number — `"1234"` is $12.34. A JSON number is an IEEE double and money does not survive one. Every amount is USD. - **Dates are ISO 8601** strings in UTC, always with the `Z`. - **Order ids are the `o_…` form.** That is the only order identifier that exists outside our database, and it is what every route takes and returns. - **Every response carries `cache-control: no-store`.** These answers are per-key by construction and must not be held by a shared cache. - **Every response carries `x-request-id`.** Log it. Quoting it turns a support conversation about "an order this morning" into one about a single request. - **No upstream is ever named.** Which carrier or supplier fills an order is not part of this API and appears in no field. Do not build logic that depends on inferring it. > **Every path ends in a slash. Read this before your first POST.** > > `POST /api/v1/orders` — without the trailing slash — answers **308 Permanent Redirect** to `/api/v1/orders/`. A 308 preserves the method and the body, so a correct client follows it and the order is placed exactly once. > > **Several HTTP libraries drop the request body when they follow a redirect.** When that happens the second request arrives empty, and the failure reads as "the API rejected my order for no reason". Write the slash and the hop never happens. The same applies to every GET. This API is for server-to-server use and sends no CORS headers. Calling it from a browser will fail, and that is the intended behavior: a key that reaches a browser is a key your customers can read. ## Idempotency > **`Idempotency-Key` is required on every purchase.** > > A request without it is a **400**. We do not generate one for you, and that is the whole point: a key the server invents is a fresh key on every attempt, so every retry would become a second order — which on this product means a second debit and a second number. - Generate one key per **purchase intent**, not per HTTP attempt. A UUID is ideal. It may be 8 to 200 characters of letters, digits and `_ . : -`. - **Retry with the same key and the same body** and you get the same order back, with nothing charged twice. This is the safe behavior after a timeout, a dropped connection or a 5xx — and after a timeout it is the *only* safe behavior, because a request that timed out may well have succeeded. - **Reuse a key with a different body** and the request is refused with **409** rather than being answered with the earlier order. Changing what you are buying means a new key. - Keys are scoped to your account, so yours can never collide with another customer's, and nobody can probe whether you used a particular value. - Reordering the JSON, adding whitespace, or sending `"serviceSlug": null` instead of omitting it all count as the **same** purchase. A retry that re-serialises its body is still a retry. Store the key alongside your own order record before you send the request, not after. A key you generated and then lost is a key you cannot retry with. ## Pagination `GET /orders/` is the only route that pages. It returns up to `limit` orders newest-first, plus a `nextCursor`. - `limit` is 1 to 200 and defaults to 50. - `nextCursor` is an **opaque** string. Send it back as `?cursor=` for the next page. Do not parse it, do not build one, and do not assume its format is stable. - `nextCursor` is `null` when you have reached the end. That is the only stop condition — an empty page is not one, and neither is a page shorter than `limit`. - Keep `family`, `open` and `limit` identical for the whole walk. Changing a filter mid-walk makes the cursor mean something you did not intend. - A malformed cursor is a **400**, never a silent restart at page one. An integration that silently restarts loops over the same page forever and nobody notices until the bill does. Ordering is by creation time, newest first. New orders appear at the *front*, so a walk that takes a while will not see orders created after it started — which is what you want when you are reconciling. ## Rate limits The limit is **per key, per 60-second window**, and it is a property of the key. Issue one key per integration and a burst in one will not throttle the others. Your own limit is on the API root response, so you never have to discover it by hitting it. ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 57 X-RateLimit-Reset: 1755273600 unix seconds, when the window resets ``` Over the limit is **429** with a `Retry-After` header in seconds. Obey that header rather than a fixed back-off — it is computed from the database clock, so every one of our instances agrees on it. When you are polling an order, poll on a schedule you can defend. There are no outbound webhooks in this version, so polling is the mechanism — but a tight loop spends your window on nothing and will throttle the purchase you actually needed to make. ## Errors Every failure is `{ "error": { "code", "message" } }` at the status below. **Switch on `code`, never on `message`** — the code is the contract and the message is prose we may reword. `retryable` means repeating the identical call may succeed. Anything else needs a change on your side, or a report to us with the `x-request-id`. ```http HTTP/1.1 402 { "error": { "code": "insufficient_funds", "message": "Insufficient balance", "details": { "balanceCents": "120", "requiredCents": "150", "shortfallCents": "30" } } } ``` A `details` object appears only on the codes marked below, and only those. It is part of the published contract for those codes and is not a general debugging channel. | Code | HTTP | Retryable | `details` | What it means | | --- | --- | --- | --- | --- | | `bad_request` | 400 | no | no | The request is malformed — a missing header, an unparseable body, an unknown filter value. | | `config_error` | 500 | no | no | A server-side misconfiguration. Not a client error; report it. | | `forbidden` | 403 | no | no | The key is valid but its `permissions` do not include the one this route requires. | | `idempotency_conflict` | 409 | no | no | This `Idempotency-Key` already names a different order. Use a fresh key for a new order. | | `illegal_transition` | 500 | no | no | The order was in an unexpected status when the change was applied. Nothing was charged. Report it if it repeats. | | `insufficient_funds` | 402 | no | yes | The account balance does not cover the order. `details` carries the shortfall in cents. | | `internal_error` | 500 | no | no | An unhandled server-side failure. Report it with the time of the call. | | `invalid_amount` | 500 | no | no | A price or amount failed validation. Server-side; report it. | | `invalid_argument` | 400 | no | no | A pagination or filter argument was out of range or the wrong shape. | | `invalid_ledger_entry` | 500 | no | no | A ledger entry failed validation. Server-side; report it. | | `invalid_order_input` | 500 | no | no | An order field was rejected before anything was charged. | | `invalid_pricing_rule` | 500 | no | no | A pricing rule is malformed. Server-side; report it. | | `invalid_refund_reason` | 500 | no | no | A refund was attempted with an unknown reason. Server-side; report it. | | `invalid_signature` | 401 | no | no | A webhook signature did not verify. Not reachable from `/api/v1`. | | `ledger_post_failed` | 500 | no | no | A balance movement could not be recorded. Nothing was charged; the request is safe to retry. | | `not_found` | 404 | no | no | No such route, order, country, service or destination for this key's account. | | `order_create_conflict` | 409 | yes | no | The `Idempotency-Key` collided with an order that then rolled back. Retry with the same key. | | `order_family_mismatch` | 500 | no | no | An order was routed to the wrong family's provisioner. Server-side; report it. | | `order_not_cancellable` | 409 | no | no | The order is in a status that cannot be cancelled — it is already completed, expired, failed or cancelled. | | `order_not_found` | 404 | no | no | The order named by the request does not exist. | | `order_transition_failed` | 500 | no | no | The order's status could not be advanced. Server-side; report it. | | `out_of_stock` | 409 | no | no | No provider could supply this product right now. Nothing was charged. | | `provider_not_registered` | 500 | no | no | No adapter is registered for the provider this order needs. Server-side; report it. | | `provider_unavailable` | 503 | yes | no | A provider's circuit breaker is open. Safe to retry shortly. | | `rate_limited` | 429 | yes | yes | Over the key's `rate_limit` for the current 60-second window. `Retry-After` says when to try again. | | `rental_not_priced` | 409 | no | no | No pricing rule covers this rental term, so no price could be quoted. Nothing was charged. | | `sms_already_received` | 409 | no | no | This order has already received an SMS, so it cannot be cancelled or refunded. The code you were sent is yours. | | `unauthorized` | 401 | no | no | No `Authorization: Bearer sk_live_…` header, or the key is unknown, revoked, expired, or belongs to a blocked account. | | `upstream_error` | 502 | no | no | A provider rejected or failed the call. Nothing was charged, or the charge was refunded. | | `upstream_timeout` | 504 | yes | no | A provider did not answer in time. Safe to retry. | | `user_not_found` | 404 | no | no | The account behind the key could not be read. Server-side; report it. | > **Nothing is charged on a failure.** > > A purchase that fails before the debit never takes the money; a purchase that fails after it is refunded automatically. If you ever see a debit with no corresponding order, that is a bug and we want the `x-request-id`. ## OpenAPI, Postman and llms.txt This page, the OpenAPI document, the Postman collection and the `llms.txt` reference are all generated from one description of the API. They cannot disagree with each other, and none of them can disagree with the routes. - [OpenAPI 3.1](/reseller/api/openapi.json) — import into your generator of choice for a typed client in any language. - [Postman collection](/reseller/api/simarea.postman_collection.json) — import straight into Postman or Insomnia. Set the `apiKey` collection variable and every request is ready to send. - [llms.txt](/reseller/api/llms.txt) — the whole reference as plain Markdown, for pasting into an AI assistant that is writing your client. If you are asking a model to write your integration, give it the `llms.txt` file rather than a screenshot of this page. It contains the same rules in the same order, and none of the layout. ## Response headers Every response carries the first three. The rate-limit window is on the answers that were counted against it, and `retry-after` exists only on the refusal. | Header | On | Type | Description | | --- | --- | --- | --- | | `x-request-id` | every response | `string` | This request's id. Log it. Quoting it turns a support conversation about "an order this morning" into one about a single request. An id you send yourself is echoed back when it is a plausible shape; otherwise we mint one. Example: `3f0b7c11-4a2e-4c6d-9d24-a7e0e5b1c9f4`. | | `x-api-version` | every response | `string` | The version segment that served the request — the same `v1` that is in the path. Example: `v1`. | | `cache-control` | every response | `string` | Always `no-store`. Every answer here is per-key by construction and must never be held by a shared cache. Example: `no-store`. | | `x-ratelimit-limit` | responses counted against the window | `integer` | Requests your key is allowed in a 60-second window. Example: `60`. | | `x-ratelimit-remaining` | responses counted against the window | `integer` | Requests left in the current window. Example: `57`. | | `x-ratelimit-reset` | responses counted against the window | `integer` | Unix seconds at which the window resets. Computed from the database clock, so every instance agrees. Example: `1755273600`. | | `retry-after` | `429` only | `integer` | Seconds to wait before repeating the call. Present on a 429 only, and it is the authoritative copy — obey it rather than a back-off you chose. Example: `12`. | ## eSIM data plans An eSIM is a data plan for a destination. You pick a destination, pick a plan, buy it, and the order comes back with everything the customer needs to install the profile — a QR image, an activation code and an SM-DP+ address — plus how much data it has used. > **eSIM plans are not purchasable yet, and that is deliberate.** > > `GET /catalog/packages/` returns an empty list on every account today. eSIM pricing is blocked on a unit question with our supplier that has not been settled, so no plan is priced and none is offered for sale. Your key authenticates, the route works, the list is empty — it is not a fault at your end and not a fault at ours. > > `GET /catalog/destinations/` answers in full in the meantime, and its `syncedPackageCount` tells you how many plans exist per destination regardless of pricing. Build and test your destination mapping now; plans appear the day they are priced, with no change to any shape on this page. - Read `pricingUnit` before `priceCents`: `bundle` is the whole plan, `per_day` is a daily rate and you choose the term with `periodNum`. - `dataMb` of `null` means **unlimited**, never zero. - An eSIM order is `provisioning` until the profile exists. Poll the order; `qrCodeUrl` and `acCode` fill in when it is installable. ### GET /api/v1/catalog/destinations/ — List eSIM destinations **Permission** — read. Every place an eSIM plan can cover — single countries, regions and global plans — in one list. Build your destination mapping against `syncedPackageCount`, which counts what our supplier carries regardless of whether it is priced yet. `packageCount` counts only what is on sale. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "destinations": Destination[] }`. `Destination` is under **Data shapes**. **Errors** — `unauthorized`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Example — List destinations** ```bash curl https://simarea.com/api/v1/catalog/destinations/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "destinations": [ { "slug": "thailand", "kind": "country", "name": "Thailand", "countryCode": "th", "isPopular": true, "fromBundlePriceCents": null, "fromPerDayPriceCents": null, "packageCount": 0, "syncedPackageCount": 18, "isListed": true }, { "slug": "europe-esim", "kind": "region", "name": "Europe", "countryCode": null, "isPopular": true, "fromBundlePriceCents": null, "fromPerDayPriceCents": null, "packageCount": 0, "syncedPackageCount": 26, "isListed": true } ] } ``` ### GET /api/v1/catalog/packages/ — List eSIM data plans **Permission** — read. The eSIM plans on sale, optionally for one destination. `code` is what you send back as `packageCode` when you buy. Read `pricingUnit` before you read `priceCents`. On a `bundle` the price is the total for the plan; on a `per_day` package it is a daily rate and you choose the term at purchase with `periodNum`. **Query parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `destination` | `string` | optional | Narrow to one destination slug. An unknown slug is a 404. Example: `thailand`. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "packages": Package[] }`. `Package` is under **Data shapes**. **Errors** — `unauthorized`, `not_found`, `bad_request`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - This route currently returns an EMPTY LIST, and that is the correct answer rather than a fault. eSIM pricing is blocked on an unresolved unit question with our supplier, so no plan is priced and none is offered for sale. Your key authenticates, the route works, the list is empty. An eSIM purchase attempted today is refused and nothing is charged. - The destinations route answers in full in the meantime, so the mapping half of your integration can be built and tested now. **Example — Plans for one destination** ```bash curl "https://simarea.com/api/v1/catalog/packages/?destination=thailand" \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "packages": [] } ``` ### POST /api/v1/orders/ — Buy **Permission** — order. One route, two bodies, told apart by `family`. The body carries no price: the order is priced from live supply at the moment of purchase, and the response tells you what was actually charged. A success is a 201 with the order read back AFTER it was created, so its `status` is current rather than the one it held a moment earlier. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | | `Idempotency-Key` | required | One value per purchase intent, NOT per HTTP attempt. 8 to 200 characters of letters, digits and `_ . : -`; a UUID is ideal. Absent is a 400 — we never generate one for you. Example: `7c1f9a2e-purchase-001`. | **Request body** — `application/json`. Which fields apply depends on `family`; the column below says whether the KEY may be absent, and the prose says which family requires it. | Field | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | required | Which product you are buying. It decides which of the fields below apply. One of `virtual_number`, `esim`. | | `countrySlug` | `string` | optional | `virtual_number` only, and required there. From the countries route. Example: `"usa"`. | | `serviceSlug` | `string \| null` | optional | `virtual_number` only, and optional there — send `null` or omit it for a line that accepts any service. Example: `"telegram"`. | | `duration` | `string` | optional | `virtual_number` only, and required there. The term you are renting for. One of `halfDay`, `oneDay`, `3Days`, `oneWeek`, `twoWeeks`, `oneMonth`, `threeMonths`, `sixMonths`, `twelveMonths`. | | `autoRenew` | `boolean` | optional | `virtual_number` only. Defaults to `false`. When `true` we charge your balance for another term as this one ends, so the line does not lapse. | | `destinationSlug` | `string` | optional | `esim` only, and required there. From the destinations route. Example: `"thailand"`. | | `packageCode` | `string` | optional | `esim` only, and required there. The `code` of a package, sent back verbatim. | | `periodNum` | `integer \| null` | optional | `esim` only, and only on a package whose `pricingUnit` is `per_day`: how many days you are buying. Ignored on a bundle. | **Response** `201` — `{ "order": Order }`. `Order` is under **Data shapes**. **Errors** — `unauthorized`, `forbidden`, `bad_request`, `not_found`, `insufficient_funds`, `out_of_stock`, `idempotency_conflict`, `rate_limited`, `upstream_error`, `upstream_timeout`, `provider_unavailable`. Statuses and meanings are in the error table under **Errors**. **Notes** - The permission required is `order`, for a virtual number and an eSIM alike. A key without it gets a 403, and nothing is charged. - A slug that does not resolve is a 404. It is never quietly substituted for something near it. - Nothing is charged on any failure. Where a debit has already happened when the supplier fails, it is refunded automatically. **Example — Buy an eSIM data plan** ```bash curl -X POST https://simarea.com/api/v1/orders/ \ -H "Authorization: Bearer sk_live_..." \ -H "Idempotency-Key: 9d24a7e0-esim-001" \ -H "Content-Type: application/json" \ -d '{ "family": "esim", "destinationSlug": "thailand", "packageCode": "TH-7D-1GB", "periodNum": 7 }' ``` Response `201`: ```json { "order": { "publicId": "o_2wF6y9Bk4mTd", "family": "esim", "status": "provisioning", "priceCents": "420", "createdAt": "2026-08-24T09:31:47.104Z", "completedAt": null, "esim": { "destinationSlug": "thailand", "qrCodeUrl": null, "acCode": null, "smdpAddress": null, "iccid": null, "dataTotalMb": "1024", "dataUsedMb": "0", "expiresAt": null } } } ``` ### GET /api/v1/orders/{publicId}/ — Read one order **Permission** — read. One order and everything currently known about it, including every SMS received on a number order and the install details on an eSIM. This is the route you poll while an order is in flight. There are no outbound webhooks in this version. **Path parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `publicId` | `string` | required | The order's `o_…` id. Example: `o_8kQ2m4Xr9dLp`. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "order": Order }`. `Order` is under **Data shapes**. **Errors** — `unauthorized`, `not_found`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - An order belonging to another account answers 404, not 403. A key can only ever see its own account's orders. - Poll no faster than the rate-limit headers allow. **Example — An installable eSIM** ```bash curl https://simarea.com/api/v1/orders/o_2wF6y9Bk4mTd/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "order": { "publicId": "o_2wF6y9Bk4mTd", "family": "esim", "status": "active", "priceCents": "420", "createdAt": "2026-08-24T09:31:47.104Z", "completedAt": null, "esim": { "destinationSlug": "thailand", "qrCodeUrl": "https://qr.example.net/8ac1f0e2b7.png", "acCode": "LPA:1$smdp.example.net$K2-XXXXXXXX", "smdpAddress": "smdp.example.net", "iccid": "8900000000000000000", "dataTotalMb": "1024", "dataUsedMb": "37", "expiresAt": "2026-08-31T09:33:02.000Z" } } } ``` ### GET /api/v1/orders/ — List your orders **Permission** — read. Your orders, newest first, across every family unless you narrow it. This is also the eSIM list and the number list — there is no separate route for either, because two doors onto one list are two things to keep in step. Pages are walked with `cursor`. Send the `nextCursor` from the previous response to get the next page; `nextCursor` is `null` on the last one. **Query parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | optional | Only this product family. Absent means every family. One of `virtual_number`, `esim`. | | `open` | `boolean` | optional | `1` or `true` returns only orders still in flight. Absent means no filter, which is not the same request as `open=0`. | | `limit` | `integer` | optional | 1 to 200. Defaults to 50. Example: `100`. | | `cursor` | `string` | optional | The `nextCursor` from the previous page. Opaque — do not parse it or construct one. A malformed value is a 400 rather than a silent restart at page one. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "orders": Order[] }`. `Order` is under **Data shapes**. Returned alongside `orders`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `nextCursor` | `string \| null` | required | Pass as `?cursor=` for the next page. `null` when this was the last page. Present only alongside `orders`. | **Errors** — `unauthorized`, `bad_request`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - Keep the filters identical while you page. Changing `family` or `open` mid-walk makes the cursor mean something you did not intend. **Example — Your eSIMs** ```bash curl "https://simarea.com/api/v1/orders/?family=esim" \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "orders": [], "nextCursor": null } ``` **Example — Page through every order** ```bash curl "https://simarea.com/api/v1/orders/?limit=200" \ -H "Authorization: Bearer sk_live_..." # then, while nextCursor is not null: curl "https://simarea.com/api/v1/orders/?limit=200&cursor=MjAyNi0wOC0yNFQwOToxNDowMy4yMjFafG9fOGtRMm00WHI5ZExw" \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "orders": [ /* 200 orders */ ], "nextCursor": "MjAyNi0wOC0yNFQwOToxNDowMy4yMjFafG9fOGtRMm00WHI5ZExw" } ``` ### POST /api/v1/orders/{publicId}/cancel/ — Cancel and refund an order **Permission** — read, plus order. Stops an order and returns what it refunded. The rules are exactly the website's own cancel button — this route is not a second policy. **Path parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `publicId` | `string` | required | The order's `o_…` id. Example: `o_8kQ2m4Xr9dLp`. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "cancel": Cancel }`. `Cancel` is under **Data shapes**. **Errors** — `unauthorized`, `forbidden`, `not_found`, `sms_already_received`, `order_not_cancellable`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - Canceling an order that our own expiry sweeper closed a moment earlier answers 200, not an error. You asked for it to stop; it has stopped. - An order that has already received an SMS cannot be canceled or refunded — the code you paid for arrived. That is a 409. - A rental that ran its full term refunds nothing, because it delivered everything it promised. **Example — Already closed by the sweeper** ```bash curl -X POST https://simarea.com/api/v1/orders/o_8kQ2m4Xr9dLp/cancel/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "cancel": { "status": "expired", "refundedCents": "0" } } ``` ## Virtual numbers A virtual number is a dedicated non-VoIP line you rent for a term — from half a day up to a year. It is yours for that whole term, it takes messages from any service unless you rented it for a specific one, and its full message history stays on the order. - Send `serviceSlug: null` (or omit it) for a line that accepts **any** service. Name a service only when you need one guaranteed to work with it. - `autoRenew: true` charges your balance for another term as the current one ends, so the line does not lapse. It defaults to `false`. - Canceling mid-term follows the same rules as the website's cancel button. A rental that ran its full term refunds nothing — it delivered what it promised. - Messages arrive on the order. Poll `GET /orders/{publicId}/` and read the `rental.messages` array. ### GET /api/v1/catalog/countries/ — List every country **Permission** — read. Every country we sell numbers in, popular ones first — the same order the website uses, so a list you cache and a page a customer reads agree. It is NOT filtered by what is in stock this minute. That is a property of one family at one moment and the offers route answers it; a country list that shrank when a pool emptied would silently break a mapping you built last week. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "countries": Country[] }`. `Country` is under **Data shapes**. **Errors** — `unauthorized`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Example — List countries** ```bash curl https://simarea.com/api/v1/catalog/countries/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "countries": [ { "slug": "usa", "name": "United States", "code": "us", "phoneCode": "+1", "isPopular": true }, { "slug": "united-kingdom", "name": "United Kingdom", "code": "gb", "phoneCode": "+44", "isPopular": true } ] } ``` ### GET /api/v1/catalog/services/ — List every service **Permission** — read. Every app or website a number can be bought for, popular ones first. `slug` is what a purchase body takes. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "services": Service[] }`. `Service` is under **Data shapes**. **Errors** — `unauthorized`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Example — List services** ```bash curl https://simarea.com/api/v1/catalog/services/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "services": [ { "slug": "telegram", "name": "Telegram", "isPopular": true }, { "slug": "whatsapp", "name": "WhatsApp", "isPopular": true } ] } ``` ### GET /api/v1/catalog/offers/ — Live prices and stock **Permission** — read. What can be bought right now and for how much. Each row is the cheapest live price for a country, a service and a term across every supplier we have enabled. `family` is required and there is no "all families" mode: the families do not share a price shape, and a single list would force you to branch on a field anyway. **Query parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | required | Which product to price. One of `virtual_number`, `esim`. | | `country` | `string` | optional | Narrow to one country slug. Example: `usa`. | | `service` | `string` | optional | Narrow to one service slug. Example: `telegram`. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "offers": Offer[] }`. `Offer` is under **Data shapes**. **Errors** — `unauthorized`, `forbidden`, `bad_request`, `not_found`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - A slug that does not exist is a 404, not an empty list. An unknown `family` is a 400. Neither is ever silently widened to "everything". - Prices move with supply. Quote from this route at the moment you show a price, and do not cache it for longer than you would be happy to be wrong for. **Example — Virtual number prices** ```bash curl "https://simarea.com/api/v1/catalog/offers/?family=virtual_number&country=usa" \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "offers": [ { "family": "virtual_number", "countrySlug": "usa", "serviceSlug": null, "durationSlug": "oneWeek", "priceCents": "900", "stock": null }, { "family": "virtual_number", "countrySlug": "usa", "serviceSlug": null, "durationSlug": "oneMonth", "priceCents": "2400", "stock": null } ] } ``` ### POST /api/v1/orders/ — Buy **Permission** — order. One route, two bodies, told apart by `family`. The body carries no price: the order is priced from live supply at the moment of purchase, and the response tells you what was actually charged. A success is a 201 with the order read back AFTER it was created, so its `status` is current rather than the one it held a moment earlier. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | | `Idempotency-Key` | required | One value per purchase intent, NOT per HTTP attempt. 8 to 200 characters of letters, digits and `_ . : -`; a UUID is ideal. Absent is a 400 — we never generate one for you. Example: `7c1f9a2e-purchase-001`. | **Request body** — `application/json`. Which fields apply depends on `family`; the column below says whether the KEY may be absent, and the prose says which family requires it. | Field | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | required | Which product you are buying. It decides which of the fields below apply. One of `virtual_number`, `esim`. | | `countrySlug` | `string` | optional | `virtual_number` only, and required there. From the countries route. Example: `"usa"`. | | `serviceSlug` | `string \| null` | optional | `virtual_number` only, and optional there — send `null` or omit it for a line that accepts any service. Example: `"telegram"`. | | `duration` | `string` | optional | `virtual_number` only, and required there. The term you are renting for. One of `halfDay`, `oneDay`, `3Days`, `oneWeek`, `twoWeeks`, `oneMonth`, `threeMonths`, `sixMonths`, `twelveMonths`. | | `autoRenew` | `boolean` | optional | `virtual_number` only. Defaults to `false`. When `true` we charge your balance for another term as this one ends, so the line does not lapse. | | `destinationSlug` | `string` | optional | `esim` only, and required there. From the destinations route. Example: `"thailand"`. | | `packageCode` | `string` | optional | `esim` only, and required there. The `code` of a package, sent back verbatim. | | `periodNum` | `integer \| null` | optional | `esim` only, and only on a package whose `pricingUnit` is `per_day`: how many days you are buying. Ignored on a bundle. | **Response** `201` — `{ "order": Order }`. `Order` is under **Data shapes**. **Errors** — `unauthorized`, `forbidden`, `bad_request`, `not_found`, `insufficient_funds`, `out_of_stock`, `idempotency_conflict`, `rate_limited`, `upstream_error`, `upstream_timeout`, `provider_unavailable`. Statuses and meanings are in the error table under **Errors**. **Notes** - The permission required is `order`, for a virtual number and an eSIM alike. A key without it gets a 403, and nothing is charged. - A slug that does not resolve is a 404. It is never quietly substituted for something near it. - Nothing is charged on any failure. Where a debit has already happened when the supplier fails, it is refunded automatically. **Example — Rent a virtual number** ```bash curl -X POST https://simarea.com/api/v1/orders/ \ -H "Authorization: Bearer sk_live_..." \ -H "Idempotency-Key: 3f0b7c11-rental-2026-08" \ -H "Content-Type: application/json" \ -d '{ "family": "virtual_number", "countrySlug": "usa", "serviceSlug": null, "duration": "oneWeek", "autoRenew": false }' ``` Response `201`: ```json { "order": { "publicId": "o_5rT8w1Nq3vHc", "family": "virtual_number", "status": "active", "priceCents": "900", "createdAt": "2026-08-24T09:20:11.550Z", "completedAt": null, "rental": { "phone": "+12025550188", "countrySlug": "usa", "serviceSlug": null, "durationSlug": "oneWeek", "rentedUntil": "2026-08-31T09:20:11.550Z", "autoRenew": false, "messages": [] } } } ``` ### GET /api/v1/orders/{publicId}/ — Read one order **Permission** — read. One order and everything currently known about it, including every SMS received on a number order and the install details on an eSIM. This is the route you poll while an order is in flight. There are no outbound webhooks in this version. **Path parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `publicId` | `string` | required | The order's `o_…` id. Example: `o_8kQ2m4Xr9dLp`. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "order": Order }`. `Order` is under **Data shapes**. **Errors** — `unauthorized`, `not_found`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - An order belonging to another account answers 404, not 403. A key can only ever see its own account's orders. - Poll no faster than the rate-limit headers allow. **Example — An installable eSIM** ```bash curl https://simarea.com/api/v1/orders/o_2wF6y9Bk4mTd/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "order": { "publicId": "o_2wF6y9Bk4mTd", "family": "esim", "status": "active", "priceCents": "420", "createdAt": "2026-08-24T09:31:47.104Z", "completedAt": null, "esim": { "destinationSlug": "thailand", "qrCodeUrl": "https://qr.example.net/8ac1f0e2b7.png", "acCode": "LPA:1$smdp.example.net$K2-XXXXXXXX", "smdpAddress": "smdp.example.net", "iccid": "8900000000000000000", "dataTotalMb": "1024", "dataUsedMb": "37", "expiresAt": "2026-08-31T09:33:02.000Z" } } } ``` ### GET /api/v1/orders/ — List your orders **Permission** — read. Your orders, newest first, across every family unless you narrow it. This is also the eSIM list and the number list — there is no separate route for either, because two doors onto one list are two things to keep in step. Pages are walked with `cursor`. Send the `nextCursor` from the previous response to get the next page; `nextCursor` is `null` on the last one. **Query parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | optional | Only this product family. Absent means every family. One of `virtual_number`, `esim`. | | `open` | `boolean` | optional | `1` or `true` returns only orders still in flight. Absent means no filter, which is not the same request as `open=0`. | | `limit` | `integer` | optional | 1 to 200. Defaults to 50. Example: `100`. | | `cursor` | `string` | optional | The `nextCursor` from the previous page. Opaque — do not parse it or construct one. A malformed value is a 400 rather than a silent restart at page one. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "orders": Order[] }`. `Order` is under **Data shapes**. Returned alongside `orders`: | Field | Type | Required | Description | | --- | --- | --- | --- | | `nextCursor` | `string \| null` | required | Pass as `?cursor=` for the next page. `null` when this was the last page. Present only alongside `orders`. | **Errors** — `unauthorized`, `bad_request`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - Keep the filters identical while you page. Changing `family` or `open` mid-walk makes the cursor mean something you did not intend. **Example — Virtual numbers you hold** ```bash curl "https://simarea.com/api/v1/orders/?family=virtual_number&open=1" \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "orders": [ { "publicId": "o_5rT8w1Nq3vHc", "family": "virtual_number", "status": "active", "priceCents": "900", "createdAt": "2026-08-19T11:02:44.008Z", "completedAt": null, "rental": { "phone": "+12025550188", "countrySlug": "usa", "serviceSlug": null, "durationSlug": "oneWeek", "rentedUntil": "2026-08-26T11:02:44.008Z", "autoRenew": true, "messages": [] } } ], "nextCursor": null } ``` **Example — Page through every order** ```bash curl "https://simarea.com/api/v1/orders/?limit=200" \ -H "Authorization: Bearer sk_live_..." # then, while nextCursor is not null: curl "https://simarea.com/api/v1/orders/?limit=200&cursor=MjAyNi0wOC0yNFQwOToxNDowMy4yMjFafG9fOGtRMm00WHI5ZExw" \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "orders": [ /* 200 orders */ ], "nextCursor": "MjAyNi0wOC0yNFQwOToxNDowMy4yMjFafG9fOGtRMm00WHI5ZExw" } ``` ### POST /api/v1/orders/{publicId}/cancel/ — Cancel and refund an order **Permission** — read, plus order. Stops an order and returns what it refunded. The rules are exactly the website's own cancel button — this route is not a second policy. **Path parameters** | Name | Type | Required | Description | | --- | --- | --- | --- | | `publicId` | `string` | required | The order's `o_…` id. Example: `o_8kQ2m4Xr9dLp`. | **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "cancel": Cancel }`. `Cancel` is under **Data shapes**. **Errors** — `unauthorized`, `forbidden`, `not_found`, `sms_already_received`, `order_not_cancellable`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Notes** - Canceling an order that our own expiry sweeper closed a moment earlier answers 200, not an error. You asked for it to stop; it has stopped. - An order that has already received an SMS cannot be canceled or refunded — the code you paid for arrived. That is a 409. - A rental that ran its full term refunds nothing, because it delivered everything it promised. **Example — Already closed by the sweeper** ```bash curl -X POST https://simarea.com/api/v1/orders/o_8kQ2m4Xr9dLp/cancel/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "cancel": { "status": "expired", "refundedCents": "0" } } ``` ## Account Two routes that are not about a product: what you can spend, and what the key in your hand is allowed to do. ### GET /api/v1/balance/ — Read your prepaid balance **Permission** — read. Everything you buy — on the API and on the website — is paid for out of one prepaid USD balance. This is that balance. Check it before a batch of purchases rather than after a failure: a purchase that cannot be covered is refused with `insufficient_funds`, and that error carries the exact shortfall so you can top up by the right amount. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "balance": Balance }`. `Balance` is under **Data shapes**. **Errors** — `unauthorized`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Example — Read the balance** ```bash curl https://simarea.com/api/v1/balance/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "balance": { "currency": "USD", "balanceCents": "12500", "referralBalanceCents": "0" } } ``` ### GET /api/v1/ — Check your key and discover the API **Permission** — read. Answers what this version is, where its machine-readable descriptions live, and exactly what the key you just used is allowed to do — including its rate limit. Call it first when you set an integration up. A 200 means the key is live, carries the permissions it lists, and belongs to an account in good standing; a 401 means it is missing, unknown, revoked, expired, or belongs to a suspended account. **Request headers** | Header | Required | Description | | --- | --- | --- | | `Authorization` | required | `Bearer` followed by your secret key. Example: `Bearer sk_live_...`. | **Response** `200` — `{ "api": ApiRoot }`. `ApiRoot` is under **Data shapes**. **Errors** — `unauthorized`, `rate_limited`. Statuses and meanings are in the error table under **Errors**. **Example — Check a key** ```bash curl https://simarea.com/api/v1/ \ -H "Authorization: Bearer sk_live_..." ``` Response `200`: ```json { "api": { "version": "v1", "documentationUrl": "https://simarea.com/reseller/api/", "openapiUrl": "https://simarea.com/reseller/api/openapi.json", "postmanUrl": "https://simarea.com/reseller/api/simarea.postman_collection.json", "families": ["virtual_number", "esim"], "key": { "keyId": "a1b2c3d4e5f6", "permissions": ["read", "order"], "rateLimit": 60 } } } ``` ## Data shapes Every object that crosses the wire. **Required** says whether the KEY is always present; a type ending in `| null` says the key can hold JSON `null`. The two are different questions and a client has to answer both. ### Country A country you can buy a number in. The `slug` is what every purchase body and every filter takes — never the name and never the ISO code. | Field | Type | Required | Description | | --- | --- | --- | --- | | `slug` | `string` | required | The identifier you buy against. Example: `"usa"`. | | `name` | `string` | required | Display name. Example: `"United States"`. | | `code` | `string` | required | ISO 3166-1 alpha-2, lower case. Example: `"us"`. | | `phoneCode` | `string` | required | International dialling prefix, with the plus. Example: `"+1"`. | | `isPopular` | `boolean` | required | Whether the site lists this country in its popular set. A display hint, nothing more. | ### Service An app or website a number can receive a verification code from. `brand_color`, `initials` and any success-rate figure are deliberately not published: the first two are presentation for our own pages, and the third mixes two upstream scales under one column and would be a number with no defined meaning. | Field | Type | Required | Description | | --- | --- | --- | --- | | `slug` | `string` | required | The identifier you buy against. Example: `"telegram"`. | | `name` | `string` | required | Display name. Example: `"Telegram"`. | | `isPopular` | `boolean` | required | Display hint. Not a ranking. | ### Offer One buyable line: the cheapest live price for a country, a service and a term, across every supplier we have enabled. This is the only route that answers *what can I buy right now, and for how much*. | Field | Type | Required | Description | | --- | --- | --- | --- | | `family` | `string` | required | Echoes the `family` you asked for. One of `virtual_number`, `esim`. | | `countrySlug` | `string \| null` | required | The country this line is in. Example: `"usa"`. | | `serviceSlug` | `string \| null` | required | The service this line covers. `null` on a virtual number line that accepts any service. Example: `"telegram"`. | | `durationSlug` | `string` | required | The term: one of the rental terms for a virtual number. Example: `"oneWeek"`. | | `priceCents` | `string \| null` | required | Integer cents as a decimal string. `null` means the line exists but is not currently priced, so it cannot be bought — it is returned rather than hidden so your reconciliation can see it. We never invent a price to fill the gap. Example: `"150"`. | | `stock` | `integer \| null` | required | How many the supplier says are left. `null` means the supplier publishes no figure at all — the line is real, priced and buyable, and its depth is simply unknown. Zero never appears: a line the supplier calls empty is not returned. Treating `null` as zero hides inventory you could have sold. | ### Destination Somewhere an eSIM data plan covers. The three kinds share one namespace: a single country, a region such as Europe, or a global plan. | Field | Type | Required | Description | | --- | --- | --- | --- | | `slug` | `string` | required | The identifier you buy against. Example: `"thailand"`. | | `kind` | `string` | required | Which of the three trees it belongs to. One of `country`, `region`, `global`. | | `name` | `string` | required | Display name. Example: `"Thailand"`. | | `countryCode` | `string \| null` | required | ISO 3166-1 alpha-2 for a single-country destination; `null` for regions and global plans. | | `isPopular` | `boolean` | required | Display hint. | | `fromBundlePriceCents` | `string \| null` | required | Cheapest whole-plan price here, in integer cents as a string. `null` when nothing is priced. | | `fromPerDayPriceCents` | `string \| null` | required | Cheapest day-pass rate here, in integer cents as a string. `null` when nothing is priced. | | `packageCount` | `integer` | required | How many plans are on sale here right now. | | `syncedPackageCount` | `integer` | required | How many plans our supplier carries here, whether or not we can price them yet. Build your destination mapping against this and it will not shift under you. | | `isListed` | `boolean` | required | Whether the destination has its own page on the website. | ### Package One eSIM data plan. `code` is what you send back as `packageCode` when you buy. | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | `string` | required | Send this back as `packageCode` to buy the plan. | | `name` | `string` | required | Display name. | | `destinationSlug` | `string \| null` | required | Which destination it covers. Example: `"thailand"`. | | `pricingUnit` | `string` | required | What `priceCents` means. `bundle` is the total for the whole plan. `per_day` is a daily rate and the term is chosen at purchase with `periodNum`. Most of the catalog is day passes, so a client that ignores this quotes a month at one day's price. One of `bundle`, `per_day`. | | `priceCents` | `string` | required | Integer cents as a decimal string, read according to `pricingUnit`. Example: `"420"`. | | `dataMb` | `string \| null` | required | Data allowance in megabytes, as a string. `null` means unlimited — never zero. | | `days` | `integer \| null` | required | Validity in days for a bundle. `null` on a day pass, where you choose the term at purchase. | ### Balance Your prepaid wallet. Both figures are integer cents as decimal strings, in USD. | Field | Type | Required | Description | | --- | --- | --- | --- | | `currency` | `string` | required | Always `USD`. One of `USD`. | | `balanceCents` | `string` | required | What you can spend on the API and the website. Integer cents as a string. Example: `"12500"`. | | `referralBalanceCents` | `string` | required | Earnings from the reward programs, held separately. Not spendable until you move it across in your account. Example: `"0"`. | ### Message One SMS received on a number order. | Field | Type | Required | Description | | --- | --- | --- | --- | | `from` | `string \| null` | required | Sender, when the supplier reports one. | | `text` | `string` | required | The full message body. | | `code` | `string \| null` | required | The verification code, extracted for you. `null` when the message carried nothing that looked like one — the full `text` is always there either way. Example: `"483920"`. | | `receivedAt` | `string` | required | ISO 8601, UTC. Format `date-time`. | ### Order Every purchase, in one shape. The two family-specific blocks are mutually exclusive: exactly one of `rental` and `esim` is present, and which one is decided by `family`. An order placed through this API is the same row as an order placed on the website. It appears in the account, it draws on the same balance, and it is refunded by the same rules. | Field | Type | Required | Description | | --- | --- | --- | --- | | `publicId` | `string` | required | The order identifier, `o_` followed by an opaque string. It is the ONLY order id that exists outside our database and it is what every route takes and returns. Example: `"o_8kQ2m4Xr9dLp"`. | | `family` | `string` | required | Which product this is. One of `virtual_number`, `esim`. | | `status` | `string` | required | Where the order is. `created`, `provisioning`, `waiting`, `active` and `received` are still in flight; the rest are finished. One of `created`, `provisioning`, `waiting`, `active`, `received`, `completed`, `cancelled`, `expired`, `failed`, `refunded`. | | `priceCents` | `string` | required | What was actually charged, in integer cents as a string. Example: `"150"`. | | `createdAt` | `string` | required | ISO 8601, UTC. Format `date-time`. | | `completedAt` | `string \| null` | required | When the order finished, or `null` while it is still in flight. Format `date-time`. | | `rental` | `OrderRental` | optional | Present only when `family` is `virtual_number`. | | `esim` | `OrderEsim` | optional | Present only when `family` is `esim`. | ### OrderRental — Order · rental The virtual number half of an order: a dedicated line held for a term. | Field | Type | Required | Description | | --- | --- | --- | --- | | `phone` | `string \| null` | required | The number in international form. | | `countrySlug` | `string` | required | Country the line is in. | | `serviceSlug` | `string \| null` | required | The service it was rented for, or `null` for a line that accepts any service. | | `durationSlug` | `string` | required | The term it was rented for. Example: `"oneWeek"`. | | `rentedUntil` | `string \| null` | required | When the term ends. Format `date-time`. | | `autoRenew` | `boolean` | required | Whether we will charge your balance for another term when this one ends. | | `messages` | `Message[]` | required | Everything received, oldest first. | ### OrderEsim — Order · eSIM The eSIM half of an order: everything the customer needs to install the profile, plus what it has used. | Field | Type | Required | Description | | --- | --- | --- | --- | | `destinationSlug` | `string` | required | What the plan covers. | | `qrCodeUrl` | `string \| null` | required | A QR image the customer can scan to install. `null` until the profile is issued. **Treat it as opaque and do not parse it** — it is issued by the provisioning system, not by us, and its host and path are not part of this contract. If you want the image served from your own domain, fetch it once when the order becomes installable and re-host it; do not hot-link it into a page you control. Format `uri`. | | `acCode` | `string \| null` | required | The activation code, for manual installation on a device that cannot scan. | | `smdpAddress` | `string \| null` | required | The SM-DP+ server address. | | `iccid` | `string \| null` | required | The profile's ICCID once it exists. | | `dataTotalMb` | `string \| null` | required | Allowance in megabytes as a string. `null` means unlimited. | | `dataUsedMb` | `string` | required | Used so far, in megabytes as a string. | | `expiresAt` | `string \| null` | required | When the plan stops working. Format `date-time`. | ### Cancel — Cancel result What a cancel actually did. | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `string` | required | The status the order ended in. Example: `"cancelled"`. | | `refundedCents` | `string` | required | What THIS call moved back to your balance, in integer cents as a string. `"0"` when the order had already been closed and refunded — by our own expiry sweeper, say — and `"0"` again for a rental that ran its full term, which is a completed sale rather than a failure to deliver. Example: `"150"`. | ### ApiRoot — API root What the API is and what your key can do with it. The cheapest way to check a key is valid, and the only way to read your own rate limit without being throttled first. | Field | Type | Required | Description | | --- | --- | --- | --- | | `version` | `string` | required | The version segment in the path. Example: `"v1"`. | | `documentationUrl` | `string` | required | This page. Format `uri`. | | `openapiUrl` | `string` | required | The OpenAPI 3.1 document. Format `uri`. | | `postmanUrl` | `string` | required | The Postman collection. Format `uri`. | | `families` | `string[]` | required | The product families this version sells. | | `key` | `ApiRootKey` | required | The key you authenticated with. | ### ApiRootKey — API root · key Your own key, as the server sees it. The secret is never echoed. | Field | Type | Required | Description | | --- | --- | --- | --- | | `keyId` | `string` | required | The public half of the key, for identifying it in support. | | `permissions` | `string[]` | required | What this key may do. | | `rateLimit` | `integer` | required | Requests allowed per 60-second window. | ### Error Every failure, at every status. Switch on `code`, never on `message`: the code is the contract and the message is prose we may reword. | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | `string` | required | The machine-readable failure. See the error reference. | | `message` | `string` | required | A human sentence. Not a contract. | | `details` | `object` | optional | Present only on the codes marked in the error reference, and only those. It is part of the published contract for those codes and is not a general debugging channel. | ## Machine-readable descriptions - **OpenAPI 3.1** — https://simarea.io/reseller/api/openapi.json - **Postman collection** — https://simarea.io/reseller/api/simarea.postman_collection.json Both are generated from the same description as this document. Import either for a typed client or a ready-to-send request set; nothing in them contradicts anything above.