{
  "openapi": "3.0.1",
  "info": {
    "title": "Kelwa Partner API",
    "description": "The Kelwa Partner API lets resellers and platforms sell Kelwa eSIMs from their own\nsystems: browse destinations and plans, buy eSIMs from a prepaid wallet, hand install\ndetails to end customers, follow usage, and manage the eSIM lifecycle.\n\nThis guide covers the conventions; the endpoint reference that follows it is\ngenerated from the deployed API, so it is always current.\n\n## At a glance\n\n| | |\n| --- | --- |\n| Base URL | `https://api.kelwaesim.com/api/kelwa/v1` |\n| Auth header | `Authorization: {key}` — the bare key, no scheme word |\n| Format | JSON in and out, UTF-8, `Content-Type: application/json` on request bodies |\n| Identifiers | `planCode` (plans), `code` (destinations), `orderNo` (orders), `iccid` (eSIMs). No numeric ids anywhere |\n| Pagination | `page` (1-based) and `pageSize` (default 20) on every list; response carries `totalItems` and `totalPages` |\n| Safe retries | `idempotencyKey` on `POST /orders` and `POST /esims/{iccid}/top-up`; a replay returns the original result with `200 OK` and `Idempotent-Replayed: true` |\n| Errors | One envelope: `{ \"error\": { \"code\", \"message\", \"details?\", \"requestId\" } }`. Branch on `code` |\n| Rate limit | 120 requests per minute per key; `429` with `Retry-After` in seconds |\n| Environment | Production only. Every call is real; see Testing |\n| OpenAPI 3 | `https://docs.kelwaesim.com/openapi/partner-v1.json` — import into Postman, Insomnia or an SDK generator |\n| Support | hello@kelwaesim.com, quoting the `requestId` of any failing call |\n\nEverything below applies to every endpoint unless stated otherwise.\n\n## Getting started\n\n1. An Admin on your Kelwa account creates an API key in the portal under\n   **Developer → API keys**. The key is shown once; store it in your secrets manager.\n2. Call `GET /ping` with the key to confirm it works.\n3. `GET /destinations` then `GET /plans?countryCode=AE` to see what you can sell\n   and at what price.\n4. `POST /orders` with a `planCode`, `quantity` and your own `idempotencyKey`. The\n   response contains the eSIM and its `iccid`.\n5. `GET /esims/{iccid}/install` for everything the end customer needs to install it:\n   one-tap links, a QR code and the activation code.\n6. `GET /esims/{iccid}` any time later for status, validity and live usage.\n\nYour first call, with your key pasted in place of `{key}`:\n\n```bash\ncurl \"https://api.kelwaesim.com/api/kelwa/v1/ping\" \\\n  -H \"Authorization: {key}\"\n```\n\n```powershell\nInvoke-RestMethod -Uri \"https://api.kelwaesim.com/api/kelwa/v1/ping\" `\n  -Headers @{ Authorization = \"{key}\" }\n```\n\n```json\n{ \"status\": \"ok\", \"accountId\": 2, \"keyPrefix\": \"kelwa_7f3aQx2k\", \"timestamp\": \"2026-11-04T08:35:45Z\" }\n```\n\nEvery endpoint in this reference has ready-to-run request samples in the\nright-hand column (cURL, PowerShell, JavaScript, Python, C#) and a **Test Request**\nbutton that sends the request from this page with the key you enter.\n\n> [!WARNING]\n> Test Request talks to the real API. An order or top-up is a real purchase charged\n> to your wallet, and a revoke cannot be undone. Explore with the read endpoints\n> (`/ping`, `/destinations`, `/plans`, `/esims`, `/wallet/balance`) and use the\n> cheapest plan if you want to test a purchase end to end.\n\n## Authentication\n\nEvery request carries your API key as the value of the `Authorization` header,\nwith no scheme word in front of it:\n\n```\nAuthorization: {key}\n```\n\n- Keys are created in the Kelwa portal under **Developer → API keys** by an account\n  Admin. A key is a single opaque string starting with `kelwa_`, shown once, at\n  creation; only its hash is stored, so a lost key is replaced, never recovered.\n- The portal lists each key by its first characters (its *prefix*, e.g.\n  `kelwa_7f3aQx2k`) with creation, last-used and revocation dates. `GET /ping`\n  returns the same prefix, so you can tell which key a server is configured with.\n- A key acts for the whole account. It can do anything an Admin can do through the\n  API surface here (buy, top up, suspend, revoke), so treat it as a production\n  credential.\n- Revoke a key in the portal to disable it immediately; the next request with it\n  gets `401 Unauthorized`. Revocation is permanent — generate a new key to continue.\n- A key cannot be used on the portal's own endpoints, and a portal login cannot be\n  used here. The two surfaces are separate.\n\n> [!IMPORTANT]\n> Keep the key on your servers. Never embed it in a mobile app, a browser bundle or\n> a public repository, and never log the full value. To rotate, create a second key,\n> move your servers to it, then revoke the first; both work during the switch.\n\n## Conventions\n\n**Format.** JSON in and out, UTF-8, `Content-Type: application/json` on request\nbodies. Unknown properties in requests are ignored. New optional properties may\nbe added to responses at any time without a version change; removing or renaming\none is a breaking change and would ship as v2.\n\n**Naming.** Fields are camelCase. Instants end in `At` (`createdAt`, `expiresAt`).\nUnits are in the name (`dataGb`, `dataBytes`, `validityDays`, `totalBytes`,\n`remainingDays`). Counts end in `Count`, links end in `Url`. `price` is what one\neSIM costs, `totalAmount` what an order cost, `amount` what a wallet entry moved.\n\n**Identifiers.** Nothing in this API carries a numeric `id`; every object is\naddressed by a business identifier you would also see in the portal.\n\n| Object | Identifier | Example | Where you use it |\n| --- | --- | --- | --- |\n| Plan | `name` (its plan code) | `CKH031` | `GET /plans/{planCode}`, `planCode` in `POST /orders` and top-ups |\n| Country | `code` (ISO 3166-1 alpha-2) | `AE` | `countryCode` on `GET /plans` and `GET /esims` |\n| Region | `code` (Kelwa's short code) | `EU-33` | `regionCode` on `GET /plans`, the coverage path |\n| Order | `orderNo` | `KO-20261104083545-1F9C2A` | `GET /orders/{orderNo}`, `orderNo` filter on `GET /esims` |\n| eSIM | `iccid` | `8910300000059819483` | every `/esims/{iccid}` endpoint |\n\nReferences use the same fields: an eSIM carries the `orderNo` of its latest\npurchase, a wallet entry the `iccid` it concerns. An eSIM that is still\n`Provisioning` (or `Failed`) has no ICCID yet and is reachable only through its\norder.\n\n**Shared shapes.** Wherever something points at a destination it uses the same\n`destination` object (`type`, `name`, `code`), and wherever countries are listed\nthey use the same country object `{ code, name, networks }`, where `networks` are\nthe operators serving that country and the network types each offers\n(`{ name, networkTypes }`) — the same data the Kelwa portal shows in a region's\nNetworks Coverage panel. That one object appears in a region's coverage and in the\n`coveredCountries` of plans and eSIMs, and it is the only place operators and\nspeeds are reported: a single-country plan has one entry, a regional plan one per\ncountry.\n\n**Destinations.** `destination.type` is `Country` or `Region`. The same values\nfilter `GET /destinations` (`type=Country` or `type=Region`); without `type` it\nlists both, regions first. Only destinations with at least one sellable plan are\nlisted.\n\n**Dates** are ISO 8601 in UTC with a trailing `Z`. Date filters (`createdFrom`,\n`createdTo`) accept a date (`2026-11-01`) and are inclusive of the whole day in UTC.\n\n**Money** is a decimal number, and every object that carries an amount also\ncarries `currency` (ISO 4217, e.g. `USD`): your wallet currency, the same for every\namount on your account.\n\n**Data.** `dataGb` is the plan's advertised size in GB (1 GB = 1000 MB), the figure\nin the plan's name. `dataBytes` is the same allowance in bytes using the binary\ngigabyte the network meters in (1 GB = 1073741824 B, so a 3GB plan is\n3221225472 bytes); it is `null` for unlimited plans. The byte counts under `usage`\nare comparable with `dataBytes`. Use `usedPercentage` for display and do not\nconvert the byte counts back into GB to compare with `dataGb`.\n\n**Enums** are strings (`\"status\": \"Active\"`), listed under Statuses below. Query\nfilters take the same names (`status=Active`).\n\n### Pagination\n\nList endpoints take `page` (1-based, default 1) and `pageSize` (default 20, no upper\nlimit) and return:\n\n```json\n{\n  \"items\": [ ... ],\n  \"page\": 1,\n  \"pageSize\": 20,\n  \"totalItems\": 197,\n  \"totalPages\": 10\n}\n```\n\nValues outside the allowed range are clamped, never rejected. Lists are newest\nfirst unless the endpoint says otherwise. To walk a whole list, request pages until\n`page` equals `totalPages`.\n\n### Idempotency\n\n`POST /orders` accepts an `idempotencyKey` (any string up to 100 characters, unique\nwithin your account). Sending the same key again returns the original order and\ndoes not charge your wallet: the response is `200 OK` with the header\n`Idempotent-Replayed: true`, where a new order is `201 Created`. The other fields\nof a replayed request are ignored.\n\n`POST /esims/{iccid}/top-up` accepts an `idempotencyKey` in the same way. Sending\nthe same key again for that eSIM buys nothing and charges nothing: the eSIM is\nreturned as it stands, with `Idempotent-Replayed: true`.\n\nKeys share one namespace per account across orders and top-ups, so use a new key\nfor every purchase. A top-up whose key was already used for a different purchase\n(an order, or another eSIM's top-up) is rejected with `409 IdempotencyKeyReused`.\n\nTwo requests with the same key arriving at the same moment are safe too. Only one\nis processed; the other gets `409 IdempotencyKeyInProgress` and buys nothing. Wait\na few seconds and send it again with the same key: by then the first has finished\nand you receive its result as a replay.\n\n> [!TIP]\n> Generate the key *before* the first attempt, store it with your own order record,\n> and reuse it on every retry after a timeout or a network error. Your own order id\n> is a good key (`shop-order-88213`, or `shop-topup-88213-1` for a top-up). Without\n> it, a retried request is a second purchase.\n\n### Errors\n\nEvery non-2xx response has the same body:\n\n```json\n{\n  \"error\": {\n    \"code\": \"InsufficientBalance\",\n    \"message\": \"Insufficient balance.\",\n    \"details\": { \"quantity\": [\"quantity must be between 1 and 50.\"] },\n    \"requestId\": \"0HNPQ9Q87UNG7:00000001\"\n  }\n}\n```\n\n- `code` is stable and is what your integration should branch on.\n- `message` is for humans and may be reworded.\n- `details` is present only for validation failures, keyed by field.\n- `requestId` identifies the request in Kelwa's logs. Include it when contacting\n  support.\n\n| HTTP | What to do |\n| --- | --- |\n| 400 | Fix the request, or the action is not possible in the resource's current state |\n| 401 | Check the key; generate a new one in the portal if it was revoked |\n| 402 | Fund the wallet in the portal, then retry with the same `idempotencyKey` |\n| 404 | The code, order number or ICCID is not in your account; re-read the list it came from |\n| 405 | Wrong HTTP method for the path |\n| 409 | `IdempotencyKeyInProgress`: retry in a few seconds with the same key. `IdempotencyKeyReused`: the key belongs to a different purchase; generate a new one. `TopUpUnresolved`: stop and contact support |\n| 415 | Send the body as `application/json` |\n| 429 | Wait the seconds given in `Retry-After`, then retry |\n| 500 | Retry reads; retry writes only with the same `idempotencyKey` |\n\n### Error codes\n\nBranch on `error.code`. The HTTP status tells you whether to fix the request\n(4xx) or retry (429, 500).\n\n| Code | HTTP | When |\n| --- | --- | --- |\n| `ValidationFailed` | 400 | A field is missing or out of range; `details` says which |\n| `ConflictingFilters` | 400 | Both `countryCode` and `regionCode` on `GET /plans` |\n| `InvalidQuantity` | 400 | `quantity` outside 1–50 |\n| `PlanUnavailable` | 400 | The plan exists but cannot be sold right now |\n| `EsimNotIssued` | 400 | The eSIM has no ICCID yet, so it cannot be topped up |\n| `InstallUnavailable` | 400 | No installable profile: the eSIM is still `Provisioning`, or is `Revoked` or `Deactivated` |\n| `NotSupported` | 400 | The action is not possible for this eSIM in its current state |\n| `OnlyActiveEsimsCanBeSuspended` | 400 | Suspend was called on an eSIM that is not `Installed` or `Active` |\n| `OnlySuspendedEsimsCanBeUnsuspended` | 400 | Unsuspend was called on an eSIM that is not `Suspended` |\n| `Unauthorized` | 401 | Missing, malformed, revoked or unknown API key |\n| `InsufficientBalance` | 402 | The wallet cannot cover the purchase |\n| `PlanNotFound` | 404 | No sellable plan with that code |\n| `DestinationNotFound` | 404 | No sellable country or region with that code |\n| `OrderNotFound` | 404 | No order with that `orderNo` in your account |\n| `EsimNotFound` | 404 | No eSIM with that ICCID in your account |\n| `RouteNotFound` | 404 | The path does not exist |\n| `MethodNotAllowed` | 405 | Wrong HTTP method for the path |\n| `IdempotencyKeyInProgress` | 409 | Another request with this `idempotencyKey` is still being processed; nothing was bought, retry shortly with the same key |\n| `IdempotencyKeyReused` | 409 | The top-up's `idempotencyKey` was already used for a different purchase |\n| `TopUpUnresolved` | 409 | An earlier top-up with this `idempotencyKey` was charged but never finished; do not retry, contact support |\n| `UnsupportedMediaType` | 415 | The request body is not `application/json` |\n| `RateLimitExceeded` | 429 | Over the per-key limit; wait `Retry-After` seconds |\n| `InternalError` | 500 | Unexpected failure; retry reads, retry writes only with the same idempotency key |\n\nA robust handler needs only four branches:\n\n1. `2xx` — done. For `POST /orders`, also check the order's `status`.\n2. `429`, `500` or `409 IdempotencyKeyInProgress`, or no response at all — wait (`Retry-After` if given, otherwise\n   a few seconds with backoff) and retry. Retry a `POST /orders` or a top-up only\n   with the same `idempotencyKey`.\n3. `401`, `402` — stop and alert a human: the key or the wallet needs attention.\n4. Any other `4xx` — a bug in the request or an impossible action; log `code`,\n   `details` and `requestId`, and do not retry unchanged.\n\n### Rate limits\n\nLimits are applied per API key, per minute, with a fixed window. The default is 120\nrequests per minute. A rejected request returns 429 with a `Retry-After` header in\nseconds. Cache destinations and plans on your side; they change rarely. Usage is\nrefreshed from the network about once an hour and served from a cache in between,\nso polling an eSIM's usage more often than that returns the same figures.\n\n## Statuses\n\nStatuses are Kelwa's own and are the same ones shown in the Kelwa portal, so a\npartner and Kelwa support always describe an eSIM the same way.\n\n### eSIM `status`\n\n| Status | Meaning | `GET /esims/{iccid}/install` |\n| --- | --- | --- |\n| `Provisioning` | Order accepted, profile being prepared | `400 InstallUnavailable` |\n| `Active` | In use: the eSIM has connected to a network | available |\n| `NotInstalled` | Issued, not yet downloaded to a device | available |\n| `Installed` | Downloaded to a device, data not yet started | available |\n| `Suspended` | Paused by you; `unsuspend` restores it | available |\n| `Consumed` | All data used | available |\n| `Expired` | Validity ended | available |\n| `Revoked` | Cancelled by you and refunded to your wallet | `400 InstallUnavailable` |\n| `Deactivated` | Cancelled by you without a refund (it had been used), or reclaimed by Kelwa because it was never installed within 30 days of purchase | `400 InstallUnavailable` |\n| `Failed` | Could not be issued; see `failureReason` | `400 InstallUnavailable` |\n| `Unknown` | Transient; re-read shortly | re-read shortly |\n\nA typical eSIM goes `Provisioning → NotInstalled → Installed → Active → Consumed`\nor `Expired`. The transitions you can cause:\n\n| From | To | Call |\n| --- | --- | --- |\n| `Installed`, `Active` | `Suspended` | `POST /esims/{iccid}/suspend` |\n| `Suspended` | what the network reports | `POST /esims/{iccid}/unsuspend` |\n| most states | `Revoked` (refunded) or `Deactivated` (not refunded) | `POST /esims/{iccid}/revoke` |\n\nEverything else is driven by the network and the end customer's device and is\nreported through the status. An eSIM that is never installed is reclaimed 30 days\nafter purchase and becomes `Deactivated`; the wallet debit stands, so deliver the\ninstall details promptly and remind customers who have not installed.\n\n### Order `status`\n\n| Status | Meaning |\n| --- | --- |\n| `Completed` | Every eSIM in the order was issued |\n| `PartiallyCompleted` | Some eSIMs issued, some failed |\n| `Failed` | No eSIM could be issued |\n\nA failed eSIM does not refund itself. The wallet debit stands and Kelwa operations\nfollow up on the failed eSIM, so do not re-order the same quantity; re-read the\norder later or contact support with the `orderNo`.\n\n### Wallet transaction `type`\n\n`Credit` (funds added), `Debit` (an order or top-up), `Refund` (money returned, e.g.\nafter a refundable revoke).\n\n## Delivering an eSIM\n\nInstall details live at one place, `GET /esims/{iccid}/install`, and have the same\nshape for every eSIM whichever network issued it, so a single delivery template\nserves your whole catalogue:\n\n```json\n{\n  \"iccid\": \"8910300000059819483\",\n  \"status\": \"Active\",\n  \"activationCode\": \"LPA:1$rsp.example.net$7BF6988BDB72400D96FA371326D442A4\",\n  \"smdpAddress\": \"rsp.example.net\",\n  \"matchingId\": \"7BF6988BDB72400D96FA371326D442A4\",\n  \"qrCode\": \"data:image/png;base64,iVBORw0KGgo…\",\n  \"iosUniversalUrl\": \"https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.net%24…\",\n  \"androidUniversalUrl\": \"https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.net%24…\",\n  \"giftLinkUrl\": null\n}\n```\n\nOffer the one-tap links first and keep the QR code and manual details as\nfallbacks:\n\n| Field | What it is | Show it when |\n| --- | --- | --- |\n| `iosUniversalUrl` | Opens the eSIM installer directly on iOS 17.4 and later | The customer is on a recent iPhone |\n| `androidUniversalUrl` | Opens the eSIM installer directly on Android | The customer is on Android |\n| `qrCode` | PNG image of the QR code as a `data:` URI, about 2 KB | The customer installs from another screen (a laptop, a printed voucher, an email) |\n| `activationCode` | The full `LPA:1$…` string | You render your own QR code, or the customer pastes it manually |\n| `smdpAddress`, `matchingId` | The two parts of the activation code | Manual entry on devices that ask for the address and code separately |\n| `giftLinkUrl` | A Kelwa gift link the customer opens to claim the eSIM in the Skyo app; `null` unless gift links are enabled on your account | You hand the eSIM over as a gift rather than installing it directly |\n\n`qrCode` can be dropped straight into an `<img src=\"…\">` or an email body; it is the\nQR of `activationCode`, nothing more, so a code you render yourself is equivalent.\n\n`validity.expiresAt` on the eSIM is fixed at purchase time, so the plan does not\nwait for the customer: show the expiry date with the install details.\n`validity.installedAt` and `validity.activatedAt` tell you when the customer\ndownloaded the profile and when it first connected; `remainingDays` counts down\nfrom first use.\n\n> [!NOTE]\n> Install details are served from the moment an eSIM is issued until it is\n> `Revoked` or `Deactivated`; outside that window the call returns\n> `400 InstallUnavailable`. Store the ICCID and call `GET /esims/{iccid}/install`\n> whenever you need to show the details again rather than caching them for long.\n\n## Endpoints\n\n| Method | Path | Purpose |\n| --- | --- | --- |\n| GET | `/ping` | Verify the key |\n| GET | `/destinations` | Countries and regions with sellable plans; `type=Country` or `type=Region` for one kind |\n| GET | `/destinations/regions/{regionCode}/coverage` | Countries and operators a region covers |\n| GET | `/plans` | Every sellable plan; `countryCode` or `regionCode` for one destination |\n| GET | `/plans/{planCode}` | One plan |\n| POST | `/orders` | Buy a plan (`planCode`, `quantity`, `idempotencyKey`) |\n| GET | `/orders` | Order history (`status`, `orderNo`, `createdFrom`, `createdTo`) |\n| GET | `/orders/{orderNo}` | One order with its eSIMs |\n| GET | `/esims` | eSIMs (`status`, `iccid`, `orderNo`, `countryCode`, `createdFrom`, `createdTo`) |\n| GET | `/esims/{iccid}` | One eSIM with usage and purchase history |\n| GET | `/esims/{iccid}/install` | Install details: activation code, QR code, one-tap links |\n| GET | `/esims/{iccid}/usage` | Live usage only |\n| GET | `/esims/{iccid}/top-up-plans` | Plans that can be added to this eSIM |\n| POST | `/esims/{iccid}/top-up` | Add a plan (`planCode`, `idempotencyKey`) |\n| POST | `/esims/{iccid}/suspend` | Pause connectivity |\n| POST | `/esims/{iccid}/unsuspend` | Resume connectivity |\n| GET | `/esims/{iccid}/revoke-eligibility` | Would a revoke be refunded right now |\n| POST | `/esims/{iccid}/revoke` | Cancel the eSIM permanently |\n| PUT | `/esims/{iccid}/label` | Set your own label |\n| GET | `/wallet/balance` | Spendable balance |\n| GET | `/wallet/transactions` | Ledger (`createdFrom`, `createdTo`) |\n\nFunding the wallet is done in the Kelwa portal (card or bank transfer), not through\nthe API.\n\n## A typical integration\n\n1. **Catalogue sync** (hourly or daily): `GET /destinations`, then `GET /plans?countryCode=…`\n   per destination you sell. Store `plan.name` (the plan code), `price`, `dataGb`,\n   `validityDays` and `coveredCountries`. Prices are your account's current prices\n   and can change, so re-read a plan at order time if you show the price to the\n   customer first.\n2. **Sell**: when a customer buys, `POST /orders` with the `planCode`, `quantity: 1`\n   and an `idempotencyKey` you generated and stored with the customer's order.\n   Check the response `status`; for `Completed`, take `esims[0].iccid`. Store it\n   and `orderNo` with the customer's order.\n3. **Deliver**: `GET /esims/{iccid}/install` and show the customer the one-tap links\n   and QR code (see Delivering an eSIM above).\n4. **Support**: `GET /esims/{iccid}` shows `status`, `validity` and `usage`;\n   `GET /esims/{iccid}/install` serves the install details again whenever the\n   customer needs them. `PUT /esims/{iccid}/label` to tag it with your customer\n   reference so Kelwa support and your team see the same name.\n5. **Upsell**: `GET /esims/{iccid}/top-up-plans`, then `POST /esims/{iccid}/top-up`.\n   The eSIM keeps its ICCID; `plan.dataGb`, `purchaseCount` and `purchases`\n   reflect the combined total.\n6. **Cancel**: `GET /esims/{iccid}/revoke-eligibility` to tell the customer whether a\n   refund applies, then `POST /esims/{iccid}/revoke`.\n7. **Reconcile** (daily): `GET /wallet/transactions?createdFrom=…` and match each\n   `Debit` to your orders by `reference` (the `orderNo`) and each `Refund` by\n   `iccid`; `balanceAfter` lets you verify the running balance.\n\nExample purchase:\n\n```http\nPOST /api/kelwa/v1/orders\nAuthorization: kelwa_7f3aQx2k…\nContent-Type: application/json\n\n{ \"planCode\": \"CKH031\", \"quantity\": 1, \"idempotencyKey\": \"shop-order-88213\" }\n```\n\n```json\nHTTP/1.1 201 Created\n{\n  \"orderNo\": \"KO-20261104083545-1F9C2A\",\n  \"status\": \"Completed\",\n  \"totalAmount\": 8.0,\n  \"currency\": \"USD\",\n  \"esimCount\": 1,\n  \"createdAt\": \"2026-11-04T08:35:45Z\",\n  \"esims\": [\n    {\n      \"orderNo\": \"KO-20261104083545-1F9C2A\",\n      \"iccid\": \"8910300000059819483\",\n      \"status\": \"Active\",\n      \"plan\": { \"name\": \"CKH031\", \"description\": \"United Arab Emirates 3GB 30Days\", \"dataGb\": 3, \"dataBytes\": 3221225472, \"validityDays\": 30, \"price\": 8.0, \"currency\": \"USD\" },\n      \"destination\": { \"type\": \"Country\", \"name\": \"United Arab Emirates\", \"code\": \"AE\" },\n      \"coveredCountries\": [{ \"code\": \"AE\", \"name\": \"United Arab Emirates\", \"networks\": [{ \"name\": \"Du\", \"networkTypes\": [\"3G\", \"4G\", \"5G\"] }] }],\n      \"validity\": { \"expiresAt\": \"2026-12-04T08:35:49Z\", \"remainingDays\": 30, \"installedAt\": null, \"activatedAt\": null },\n      \"purchaseCount\": 1,\n      \"createdAt\": \"2026-11-04T08:35:48Z\"\n    }\n  ]\n}\n```\n\nThe same purchase with a safe retry, in JavaScript:\n\n```javascript\nasync function buyEsim(planCode, shopOrderId) {\n  const body = JSON.stringify({ planCode, quantity: 1, idempotencyKey: `shop-order-${shopOrderId}` });\n\n  for (let attempt = 1; attempt <= 3; attempt++) {\n    let response;\n    try {\n      response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/orders\", {\n        method: \"POST\",\n        headers: { Authorization: process.env.KELWA_API_KEY, \"Content-Type\": \"application/json\" },\n        body,\n      });\n    } catch {\n      await sleep(2000 * attempt);          // network error or timeout: same key, try again\n      continue;\n    }\n\n    if (response.ok) return response.json(); // 201 Created, or 200 + Idempotent-Replayed on a retry\n\n    const { error } = await response.json();\n    // IdempotencyKeyInProgress: an earlier attempt with this key is still running\n    if (response.status === 429 || response.status >= 500 || error.code === \"IdempotencyKeyInProgress\") {\n      const wait = Number(response.headers.get(\"Retry-After\") ?? 2 * attempt);\n      await sleep(wait * 1000);\n      continue;\n    }\n    throw new Error(`${error.code}: ${error.message} (requestId ${error.requestId})`);\n  }\n  throw new Error(\"Kelwa order did not complete; check GET /orders?orderNo= before retrying\");\n}\n```\n\n## Usage\n\n`GET /esims/{iccid}` and `GET /esims/{iccid}/usage` return the same `usage` object:\n\n```json\n{ \"live\": true, \"totalBytes\": 3221225472, \"usedBytes\": 262144000, \"remainingBytes\": 2959081472, \"usedPercentage\": 8.1 }\n```\n\n- `live` is `true` when the figures are the network's own meter reading. Before the\n  eSIM has ever connected there is no meter yet: `live` is `false`, `totalBytes`\n  equals `plan.dataBytes` and nothing has been used.\n- The byte fields are `null` when the network's usage lookup is temporarily\n  unavailable; show the last value you have and try again later.\n- Figures are refreshed from the network about once an hour, so there is nothing\n  to gain from polling more often than that. For a \"data left\" screen, read usage\n  when the customer opens it rather than on a schedule.\n- `totalBytes` is the allowance across the original purchase and every top-up.\n\n## Webhooks\n\nWebhooks tell your system the moment something happens to an eSIM or to your\nwallet, so you do not have to poll. Kelwa sends an HTTPS `POST` to a URL you\nchoose; your server checks the signature, answers `200`, and acts on the event.\n\nThe events are the same for every eSIM, whichever network it was issued on: one\nset of event names, one payload shape, one set of status values.\n\n### How it works\n\n1. Something happens: the customer installs the eSIM, it connects for the first\n   time, its data runs out, its plan expires, or you top it up, suspend it,\n   reactivate it or revoke it.\n2. Kelwa records the event against the eSIM's ICCID. This always happens, whether\n   or not you have a webhook endpoint.\n3. If your account has an endpoint, Kelwa posts the event to it, signed with your\n   secret.\n4. Your server answers with a `2xx` status. If it does not, Kelwa tries again on\n   the schedule under **Retries and duplicates** below.\n5. The result of the delivery is shown in the portal, on the eSIM's **Events** tab.\n\n### Setting up your endpoint\n\nAn Admin sets the endpoint in the portal under **Developer → Webhook**.\n\n| Rule | Detail |\n| --- | --- |\n| One endpoint per account | Every event for the account goes to this one URL. There is no per-event subscription; read the `type` field and ignore the types you do not handle |\n| HTTPS only | An absolute `https://` URL with a valid certificate. Plain `http://` and URLs with embedded credentials are rejected |\n| Reachable from the internet | Kelwa calls it from its own servers, so `localhost` and private addresses cannot work; use a tunnel while developing |\n| Answers directly | Return the `2xx` from the URL itself. Do not redirect |\n| Signing secret | A `whsec_…` value shown in the portal next to the URL. It is created once and survives URL edits; remove and re-add the endpoint to rotate it |\n\nEvents that happen while no endpoint is set are still recorded and shown in the\nportal as **Not sent**. They are not sent later when you add an endpoint.\n\n### The request\n\nEvery delivery is a `POST` with `Content-Type: application/json` and this body:\n\n```json\n{\n  \"id\": \"9b1f0c2e7a5d4e8f8c3a6d2b1e0f4a7c\",\n  \"type\": \"esim.activated\",\n  \"createdAt\": \"2026-11-10T09:30:00Z\",\n  \"data\": {\n    \"iccid\": \"8944500000000000001\",\n    \"scope\": \"esim\",\n    \"orderNo\": \"KO-20261110093000-AB12CD\",\n    \"planCode\": \"CKH031\",\n    \"transactionNo\": \"26111009300014\",\n    \"status\": \"Active\",\n    \"previousStatus\": \"Installed\",\n    \"occurredAt\": \"2026-11-10T09:30:00Z\"\n  }\n}\n```\n\n| Field | Type | Meaning |\n| --- | --- | --- |\n| `id` | string | Unique id of the event. The same on every retry of that event; use it to ignore duplicates |\n| `type` | string | What happened, e.g. `esim.activated`. See **Event reference** below |\n| `createdAt` | string (UTC) | When the event happened. It does not change on a retry |\n| `data` | object | Details of the event. Its fields depend on `type` |\n\nand these headers:\n\n| Header | Value |\n| --- | --- |\n| `Kelwa-Event` | The event `type`, so you can route before parsing the body |\n| `Kelwa-Delivery` | The event `id` |\n| `Kelwa-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>`; see below |\n| `User-Agent` | `Kelwa-Webhooks/1.0` |\n\n### Verifying the signature\n\nAnyone who learns your URL can post to it, so check the signature before trusting\na body.\n\n1. Split `Kelwa-Signature` into `t` (Unix seconds when Kelwa sent this attempt)\n   and `v1`.\n2. Take the request body exactly as received, as raw bytes, before parsing it.\n3. Build the string `t` + `.` + body, e.g. `1731153600.{\"id\":\"9b1f…\"}`.\n4. Compute HMAC-SHA256 of that string with your `whsec_…` secret as the key,\n   hex-encoded in lowercase.\n5. Compare it with `v1` using a constant-time comparison.\n6. Reject the request if `t` is more than five minutes from your clock.\n\n```js\n// Node.js (Express: use express.raw({ type: 'application/json' }) for this route)\nconst [t, v1] = req.headers['kelwa-signature'].split(',').map(p => p.split('=')[1]);\nconst expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');\nconst valid = expected.length === v1.length\n           && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))\n           && Math.abs(Date.now() / 1000 - Number(t)) < 300;\n```\n\n```python\n# Python\nparts = dict(p.split(\"=\", 1) for p in request.headers[\"Kelwa-Signature\"].split(\",\"))\nexpected = hmac.new(SECRET.encode(), f\"{parts['t']}.\".encode() + raw_body, hashlib.sha256).hexdigest()\nvalid = hmac.compare_digest(expected, parts[\"v1\"]) and abs(time.time() - int(parts[\"t\"])) < 300\n```\n\n> [!WARNING]\n> Sign the raw body. If your framework parses the JSON and you re-serialize it,\n> the bytes differ and the signature will never match.\n\n### Responding\n\n| Your answer | What Kelwa does |\n| --- | --- |\n| Any `2xx` within 10 seconds | Marks the event delivered. The response body is ignored |\n| Any other status (`3xx`, `4xx`, `5xx`) | Counts it as a failure and retries |\n| No answer within 10 seconds | Counts it as a failure and retries |\n| Connection refused, DNS or TLS error | Counts it as a failure and retries |\n\nAnswer first, then do the work: put the event on a queue or save it, return\n`200`, and process it afterwards. A handler that calls the Kelwa API or sends an\nemail before answering will time out sooner or later, and the event will be sent\nagain.\n\n### Retries and duplicates\n\nAn eSIM event that is not acknowledged is sent again automatically:\n\n| Attempt | When |\n| --- | --- |\n| 1 | Immediately |\n| 2 | 1 minute later |\n| 3 | 5 minutes after that |\n| 4 | 30 minutes after that |\n| 5 | 2 hours after that |\n| 6 | 12 hours after that |\n\nAfter the sixth attempt, about 15 hours after the event, it is marked failed and\nis not sent again.\n\n- **Duplicates happen.** Every attempt of one event carries the same `id`. Store\n  the ids you have processed and skip one you have seen; an attempt you answered\n  too slowly is repeated even though you handled it.\n- **Order is not guaranteed.** A retried event can arrive after a newer one. Order\n  events by `data.occurredAt`, not by arrival.\n- **Each attempt is signed afresh.** `t` in the signature is the time of the\n  attempt, so a retry hours later still passes the five-minute check. `createdAt`\n  and `data.occurredAt` stay the time of the event.\n- **The API is the source of truth.** If you are unsure of an eSIM's state, read\n  `GET /esims/{iccid}`.\n\n`wallet.low_balance` is delivered the same way, with the same retries.\n\n### Delivery status in the portal\n\nOpen an eSIM in the portal and choose **Events** to see every event for that ICCID,\nnewest first. Each one shows what became of its delivery:\n\n| Shown as | Meaning | What to do |\n| --- | --- | --- |\n| **Sending** | Recorded; the first attempt has not finished yet | Nothing; it changes within seconds |\n| **Delivered** | Your endpoint answered `2xx` | Nothing. The attempt count shows whether it took a retry |\n| **Retrying** | The last attempt failed and another is scheduled | Read the error shown (`HTTP 500`, `Timed out after 10s`, `Could not connect to the endpoint`, `TLS handshake failed`) and fix the endpoint before the next attempt time |\n| **Failed** | All six attempts failed | Fix the endpoint; read the eSIM from the API to catch up, as this event will not be sent again |\n| **Not sent** | No endpoint was set when the event happened | Set an endpoint to receive future events |\n\nEach row names the bundle it is about (plan code and transaction number).\nSelecting an event shows its id, the status change, the time and HTTP status of the\nlast attempt, the next attempt time, and the exact `data` that was sent. Give\nsupport the event id when asking about a delivery.\n\n### Event reference\n\n| `type` | When it is sent | Source |\n| --- | --- | --- |\n| `esim.installed` | The profile was downloaded to a device | The network |\n| `esim.activated` | The eSIM connected to a network for the first time | The network |\n| `esim.consumed` | The data allowance ran out | The network |\n| `esim.expired` | The plan's validity ended | The network |\n| `esim.topped_up` | A plan was added, by `POST /esims/{iccid}/top-up` or in the portal | You |\n| `esim.suspended` | The eSIM was suspended, by the API or in the portal | You |\n| `esim.unsuspended` | A suspended eSIM was reactivated | You |\n| `esim.revoked` | The eSIM was revoked, by the API or in the portal | You |\n| `esim.deactivated` | Kelwa deactivated an eSIM that was never installed within 30 days of purchase | Kelwa |\n| `wallet.low_balance` | A purchase left the wallet at or below your alert threshold | Kelwa |\n\nEach eSIM event is sent once, when the status actually changes in Kelwa. The\nstatus and its event are saved together, so an event never reports a change that\n`GET /esims/{iccid}` does not show, and a change is never left without its event. Events with the network as\ntheir source arrive when Kelwa learns of the change: usually within minutes for\n`esim.installed` and `esim.activated`, and sometimes hours later for\n`esim.consumed` and `esim.expired`. Events you cause are sent as soon as\nthe action succeeds, including actions taken by a colleague in the portal.\n\nNew event types may be added at any time. Ignore a `type` you do not recognise and\nstill answer `2xx`, otherwise it will be retried.\n\n#### eSIM event `data`\n\nEvery `esim.*` event has the same eight fields:\n\n| Field | Type | Meaning |\n| --- | --- | --- |\n| `iccid` | string | The eSIM the event is about; what every `/esims/{iccid}` endpoint takes |\n| `scope` | string | `esim` when the event is about the whole eSIM (every bundle on the ICCID is affected), `bundle` when it is about only the bundle named by the next three fields |\n| `orderNo` | string | Order of the bundle the event is about. For a top-up it is the top-up's own order |\n| `planCode` | string | Plan code of that bundle |\n| `transactionNo` | string or null | Kelwa's reference for that bundle; the same value as `transactionNo` on the matching entry of `purchases` in `GET /esims/{iccid}`. `null` until the network has assigned it |\n| `status` | string | The bundle's status after the event |\n| `previousStatus` | string | Its status just before |\n| `occurredAt` | string (UTC) | When Kelwa recorded the change: the moment `GET /esims/{iccid}` started showing it. For a change Kelwa learns from the network this can be later than when it happened on the device |\n\n| `scope` | Events |\n| --- | --- |\n| `esim` | `esim.installed`, `esim.activated`, `esim.suspended`, `esim.unsuspended` |\n| `bundle` | `esim.consumed`, `esim.expired`, `esim.topped_up`, `esim.revoked`, `esim.deactivated` |\n\nAn `esim`-scoped event still carries `orderNo`, `planCode` and `transactionNo`;\nthey name the eSIM's most recent bundle, for reference.\n\n#### Events belong to a bundle\n\nOne ICCID can carry several bundles: the plan it was bought with, plus one for each\ntop-up. A `bundle`-scoped event is about exactly one of them, and `orderNo`,\n`planCode` and `transactionNo` say which. This matters once an eSIM has been\ntopped up:\n\n- `esim.consumed` and `esim.expired` name one bundle. They do not by themselves\n  mean the eSIM has stopped working: the customer may still have data on another\n  bundle. Read `GET /esims/{iccid}` for the combined picture before telling a\n  customer they are out of data.\n- `esim.topped_up` names the new bundle: its own `orderNo`, `planCode` and\n  `transactionNo`.\n- `esim.revoked` is sent once for each bundle on the ICCID, because a revoke\n  cancels all of them and each is refunded, or not, on its own.\n- `esim.deactivated` is sent once for each bundle Kelwa deactivates.\n- `esim.suspended` and `esim.unsuspended` act on the whole eSIM (`scope` is\n  `esim`). They are sent once and name its most recent bundle.\n\nSuspend, unsuspend and revoke always act on the whole eSIM, for every eSIM. There\nis no way to revoke one bundle and keep the others: `POST /esims/{iccid}/revoke`\ncancels every bundle on the ICCID.\n\nTo match an event to a purchase, compare `transactionNo` with the `purchases` of\n`GET /esims/{iccid}`. `transactionNo` is the same kind of reference for every\neSIM, whichever network issued it. On some eSIMs the network assigns it only when\nthe bundle is first used, so `esim.installed` (and a top-up that has not been used\nyet) can carry `null`; fall back to `orderNo` and `planCode`, which are always\npresent and together identify the bundle on that ICCID.\n\n`status` and `previousStatus` use the eSIM status values listed in the\n**Statuses** section. New fields may be added to `data`; ignore fields you do not\nknow.\n\n#### Status changes to expect\n\n| `type` | `previousStatus` | `status` |\n| --- | --- | --- |\n| `esim.installed` | `NotInstalled` | `Installed` |\n| `esim.activated` | `NotInstalled` or `Installed` | `Active` |\n| `esim.consumed` | `Active` or `Suspended` | `Consumed` |\n| `esim.expired` | `Active` or `Suspended` | `Expired` |\n| `esim.suspended` | `Installed` or `Active` | `Suspended` |\n| `esim.unsuspended` | `Suspended` | `Active` |\n| `esim.revoked` | Any except `Revoked` and `Failed` | `Revoked` when the price was refunded, otherwise `Deactivated` |\n| `esim.deactivated` | `NotInstalled`, `Installed` or `Suspended` | `Deactivated` |\n| `esim.topped_up` | Any | `Active` when it was `Consumed` or `Expired`; otherwise unchanged, so both fields can be equal |\n\nAn eSIM can skip a step. One that connects before the install is reported sends\n`esim.activated` with `previousStatus` `NotInstalled` and no `esim.installed`.\n\n#### `esim.topped_up`\n\nThe event is about the bundle that was added, so `orderNo`, `planCode` and\n`transactionNo` are the top-up's own. `previousStatus` is the eSIM's status before\nthe top-up:\n\n```json\n{\n  \"id\": \"2c7e5a90b1d34f6e9a8b7c6d5e4f3a2b\",\n  \"type\": \"esim.topped_up\",\n  \"createdAt\": \"2026-11-12T14:05:00Z\",\n  \"data\": {\n    \"iccid\": \"8944500000000000001\",\n    \"scope\": \"bundle\",\n    \"orderNo\": \"KO-20261112140500-9F3E21\",\n    \"planCode\": \"CKH693\",\n    \"transactionNo\": \"26111214050031\",\n    \"status\": \"Active\",\n    \"previousStatus\": \"Consumed\",\n    \"occurredAt\": \"2026-11-12T14:05:00Z\"\n  }\n}\n```\n\n#### `esim.revoked`\n\nSent once per bundle on the ICCID. Adds `refunded`: `true` when that bundle's\nprice went back to your wallet (a `Refund` entry in your wallet transactions),\n`false` when it had already been used:\n\n```json\n{\n  \"id\": \"5d8a1f3c6b2e4a7d9c0e1f2a3b4c5d6e\",\n  \"type\": \"esim.revoked\",\n  \"createdAt\": \"2026-11-13T08:20:00Z\",\n  \"data\": {\n    \"iccid\": \"8944500000000000001\",\n    \"scope\": \"bundle\",\n    \"orderNo\": \"KO-20261110093000-AB12CD\",\n    \"planCode\": \"CKH031\",\n    \"transactionNo\": \"26111009300014\",\n    \"status\": \"Revoked\",\n    \"previousStatus\": \"NotInstalled\",\n    \"occurredAt\": \"2026-11-13T08:20:00Z\",\n    \"refunded\": true\n  }\n}\n```\n\n#### `esim.deactivated`\n\nSent when Kelwa deactivates an eSIM that was never installed within 30 days of\npurchase (see Statuses). The wallet debit stands, so `refunded` is always `false`.\nRemind customers who have not installed before the 30 days are up; after this\nevent the install details no longer work.\n\nThis event is only ever sent for that 30-day deactivation. A revoke you ask for\nalways sends `esim.revoked`, including when it is not refunded and the eSIM's\nstatus also ends as `Deactivated`: the event tells you who acted, the status tells\nyou where the eSIM ended up.\n\n```json\n{\n  \"id\": \"7a3c9e1d5b4f4c2a8e6d0b9f1a2c3d4e\",\n  \"type\": \"esim.deactivated\",\n  \"createdAt\": \"2026-12-10T00:05:00Z\",\n  \"data\": {\n    \"iccid\": \"8944500000000000001\",\n    \"scope\": \"bundle\",\n    \"orderNo\": \"KO-20261110093000-AB12CD\",\n    \"planCode\": \"CKH031\",\n    \"transactionNo\": \"26111009300014\",\n    \"status\": \"Deactivated\",\n    \"previousStatus\": \"NotInstalled\",\n    \"occurredAt\": \"2026-12-10T00:05:00Z\",\n    \"refunded\": false\n  }\n}\n```\n\n#### `wallet.low_balance`\n\nThe account owner sets a threshold under **Account settings → Alerts** in the\nportal, with the email address that should be told (the owner's own email by\ndefault). When a purchase leaves the wallet at or below that amount, Kelwa sends\none email and one `wallet.low_balance` delivery with the balance and threshold at\nthat moment. It will not fire again until a top-up lifts\nthe balance above the threshold and a later purchase drops it back down. Use it to\ntrigger a top-up or to pause sales before `POST /orders` starts returning\n`InsufficientBalance`.\n\nIts `data` is different from the eSIM events:\n\n```json\n{\n  \"id\": \"6f1c2d8e4b0a4c7f9d3e2a1b5c6d7e8f\",\n  \"type\": \"wallet.low_balance\",\n  \"createdAt\": \"2026-11-09T12:00:00Z\",\n  \"data\": { \"balance\": 45.5, \"threshold\": 50, \"currency\": \"USD\", \"occurredAt\": \"2026-11-09T12:00:00Z\" }\n}\n```\n\n| Field | Meaning |\n| --- | --- |\n| `balance` | Wallet balance right after the purchase that triggered the alert |\n| `threshold` | The alert threshold set in the portal |\n| `currency` | ISO 4217 code of both amounts |\n| `occurredAt` | When the balance crossed the threshold (UTC) |\n\nIt is retried on the same schedule as the eSIM events. It is about your account\nrather than one eSIM, so it does not appear on any eSIM's Events tab. The email is\nsent once, whatever happens to the webhook.\n\n### Handling events well\n\nA receiver that follows these rules will not double-process or miss an event:\n\n1. **Verify** the signature on the raw body, and reject a timestamp more than five\n   minutes off.\n2. **Acknowledge first.** Save or queue the event and answer `2xx` within 10\n   seconds; process it afterwards.\n3. **De-duplicate** on `id`. Processing the same id twice must be harmless.\n4. **Do not rely on order.** Compare `data.occurredAt` with what you last applied\n   for that ICCID and skip an older event.\n5. **Ignore what you do not know**: unknown `type` values and unknown fields.\n6. **Reconcile.** Webhooks can fail for up to 15 hours and then stop. Re-read\n   `GET /esims/{iccid}` when a customer opens their eSIM, and after any outage of\n   your endpoint.\n\n### Trying it out\n\nThere is no test button, and no sandbox: events come from real eSIMs. Two ways to\nsee a delivery within seconds:\n\n- On an eSIM that is already installed, suspend and then unsuspend it, from the\n  portal or with `POST /esims/{iccid}/suspend` and `/unsuspend`. That sends\n  `esim.suspended` and `esim.unsuspended` and costs nothing.\n- On a new eSIM that nobody has installed, top it up and then revoke it. That sends\n  `esim.topped_up` and one `esim.revoked` per plan, and an unused eSIM is refunded\n  in full when revoked.\n\nThen open the eSIM's **Events** tab in the portal to see whether your endpoint\naccepted them and the exact `data` that was posted.\n\nWhile developing on your own machine, expose your handler through an HTTPS tunnel\nand set the tunnel's URL as the endpoint.\n\n## Testing\n\nThere is one environment, production: real plans, a real wallet, real eSIMs.\nTo test end to end, keep a small balance in the wallet, buy the lowest-price\nplan, and revoke the test eSIM afterwards where `GET /esims/{iccid}/revoke-eligibility`\nsays it is refundable. The read endpoints can be exercised freely.\n\nA checklist before going live:\n\n- The key is read from configuration on the server, never shipped to a client.\n- Every `POST /orders` and every top-up sends an `idempotencyKey` that is stored\n  before the call.\n- `429` and `500` are retried with backoff; other `4xx` are logged with `requestId`.\n- Orders with `status` other than `Completed` are surfaced to your operations team.\n- Install details are fetched from `GET /esims/{iccid}/install` when needed, never\n  stored long-term; customers receive them promptly, and you remind those who have\n  not installed before the 30-day reclaim.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| `401 Unauthorized` on every call | The header carries a scheme word (`Bearer kelwa_…`), the key was revoked, or a portal login token was used | Send the bare key; check the key's status in the portal under Developer → API keys |\n| `404 EsimNotFound` for an eSIM you just bought | The eSIM is still `Provisioning` and has no ICCID yet | Read it through `GET /orders/{orderNo}` until it has an `iccid` |\n| `404 PlanNotFound` on a plan you see in the portal | The plan is not sellable to your account, or you passed a title instead of the code | Use the plan's `name` from `GET /plans` |\n| `400 ConflictingFilters` | Both `countryCode` and `regionCode` were sent | Send one or the other |\n| `200 OK` instead of `201 Created` from `POST /orders` | The `idempotencyKey` was used before; this is the original order | Expected on a retry. If it was not a retry, your keys are not unique |\n| `409 IdempotencyKeyInProgress` | Your first request with this key is still running (often a retry sent after a client-side timeout) | Wait a few seconds and retry with the same key; you get the first request's result |\n| `409 TopUpUnresolved` | An earlier top-up with this key debited your wallet but was interrupted before it finished | Do not retry or use a new key; send support the `requestId` and the ICCID so the top-up is completed or refunded |\n| A `Debit` immediately followed by a `Refund` for the same top-up | The network refused the top-up after your wallet was debited, and the amount was returned | Nothing to fix; the call returned an error and can be retried with the same key |\n| `409 IdempotencyKeyReused` on a top-up | The `idempotencyKey` was already used for an order or for another eSIM's top-up | Generate a new key for every purchase |\n| `usage.live` is `false` and `usedBytes` is `0` | The eSIM has not connected to a network yet | Nothing to fix; `installedAt` and `activatedAt` show when it does |\n| Customer cannot scan the QR code | Screenshots of QR codes cannot be scanned by the same phone | Send `iosUniversalUrl` / `androidUniversalUrl` to the phone instead |\n| `400 InstallUnavailable` | The eSIM is still `Provisioning`, or it is `Revoked` or `Deactivated` | Check `status`; for `Provisioning`, re-read the order shortly |\n| `415 UnsupportedMediaType` | The request body was not sent as JSON | Set `Content-Type: application/json` |\n\n## Glossary\n\n| Term | Meaning |\n| --- | --- |\n| eSIM | A SIM profile downloaded to a device instead of a physical card. One eSIM is one ICCID |\n| ICCID | The eSIM's 19–20 digit identifier, assigned when it is issued; the eSIM's identifier in this API |\n| Plan | A data allowance with a validity, sold for a destination; identified by its plan code (`name`) |\n| Plan code | The stable identifier of a plan, e.g. `CKH031`; the value of `planCode` on orders and top-ups |\n| Destination | A country (ISO code) or a Kelwa region (`EU-33`) that plans are sold for |\n| Coverage | The countries a plan works in, with the operators and network types in each |\n| Top-up | A further plan added to an existing ICCID; the eSIM keeps its install details |\n| Install details | What `GET /esims/{iccid}/install` returns: activation code, its SM-DP+ address and matching id, QR code, one-tap links and the gift link when enabled |\n| LPA string | `LPA:1$<smdp>$<code>`, the standard activation string an eSIM installer or QR code carries |\n| SM-DP+ | The server a device downloads the profile from; its address is part of the LPA string |\n| Validity | The period during which the plan can be used; fixed at purchase, counted down from first use |\n| Revoke | Permanently cancel an eSIM; refunded to the wallet only when `revoke-eligibility` says so |\n| Wallet | Your prepaid balance, funded in the portal, debited by orders and top-ups |\n\n## Versioning\n\nThe version is in the path (`/api/kelwa/v1`). Additive changes (new endpoints,\nnew optional fields, new enum values on *input*) ship in place. Anything that would\nchange the meaning of an existing field or status, or add a status value a client\nmight not handle, ships as a new path version with a migration window.\n\n## Changelog\n\n| Version | Date | Changes |\n| --- | --- | --- |\n| v1 | 2026-11-04 | Initial release: destinations, plans, orders, eSIM lifecycle (top-up, suspend, unsuspend, revoke, label), usage, wallet balance and ledger. |\n| v1 | 2026-11-09 | Install details moved to `GET /esims/{iccid}/install`, identical for every eSIM; `qrCode` is a PNG data URI. eSIM and order payloads no longer carry an `install` block. `giftLinkUrl` moved from the eSIM payload to the install details. |\n| v1 | 2026-11-09 | Webhooks live: one `https://` endpoint per account (portal, **Developer → Webhook**), signed deliveries, first event `wallet.low_balance` driven by the new **Account settings → Alerts** low-balance setting (account owner only). |\n| v1 | 2026-11-10 | `POST /esims/{iccid}/top-up` accepts an optional `idempotencyKey`: a replay returns the eSIM with `Idempotent-Replayed: true` and charges nothing; a key used for a different purchase is `409 IdempotencyKeyReused`. On both `POST /orders` and top-ups, a request whose key is still being processed by an earlier request now gets `409 IdempotencyKeyInProgress` instead of risking a second purchase. A top-up now debits the wallet before the network adds the plan and refunds it at once if the network refuses (a `Debit` + `Refund` pair in the ledger); a top-up interrupted mid-way is refused on retry with `409 TopUpUnresolved`. |\n\n| v1 | 2026-11-10 | eSIM events on the webhook endpoint: `esim.installed`, `esim.activated`, `esim.consumed`, `esim.expired`, `esim.topped_up`, `esim.suspended`, `esim.unsuspended`, `esim.revoked`, one shape for every eSIM. Failed deliveries are retried up to five times over about 15 hours with the same event `id`; the signature timestamp is now the time of the attempt. The portal's eSIM panel has an **Events** tab showing each event's delivery result. Every event names the bundle it is about (`orderNo`, `planCode`, `transactionNo`), and each entry of `purchases` in `GET /esims/{iccid}` now carries its `transactionNo`. |\n\n| v1 | 2026-11-10 | Events now carry `scope` (`esim` or `bundle`). New event `esim.deactivated` for an unused eSIM that Kelwa deactivates after 30 days. `esim.consumed` and `esim.expired` are sent for every eSIM on a regular schedule, no longer only when the eSIM is read. |\n| v1 | 2026-11-11 | `POST /esims/{iccid}/suspend` accepts only `Installed` and `Active` eSIMs. An eSIM that has not been installed returns `400 OnlyActiveEsimsCanBeSuspended`; before, it failed with a server error on some eSIMs. |\n| v1 | 2026-11-11 | `wallet.low_balance` is now retried on the same schedule as the eSIM events (it was sent once). The alert email is unchanged. |\n\nAdditions are announced here and never change existing fields (see Versioning above).",
    "contact": {
      "name": "Kelwa partner support",
      "email": "hello@kelwaesim.com"
    },
    "version": "v1"
  },
  "servers": [
    {
      "url": "https://docs.dev.kelwaesim.com",
      "description": "This environment"
    }
  ],
  "paths": {
    "/api/kelwa/v1/destinations": {
      "get": {
        "tags": [
          "Destinations"
        ],
        "summary": "List destinations",
        "description": "Only destinations with at least one sellable plan are returned. Without\r\n`type` the list holds both kinds, regions first and then countries, each\r\ngroup alphabetical; pass `type=Country` or `type=Region` for one\r\nkind only.",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "`Country` or `Region`. Omit for both.",
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/DestinationType"
                }
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive match on name or code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedListOfDestination"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/destinations\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/destinations\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/destinations\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/destinations\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/destinations\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/destinations/regions/{regionCode}/coverage": {
      "get": {
        "tags": [
          "Destinations"
        ],
        "summary": "Get a region's coverage",
        "description": "Which countries a region's plans cover and which operators serve them. Countries have no separate coverage call — a country plan covers that country.",
        "parameters": [
          {
            "name": "regionCode",
            "in": "path",
            "description": "Region code (`code` of a Region destination), e.g. `EU-33`.",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "EU-33"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DestinationCoverage"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/destinations/regions/EU-33/coverage\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/destinations/regions/EU-33/coverage\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/destinations/regions/EU-33/coverage\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/destinations/regions/EU-33/coverage\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/destinations/regions/EU-33/coverage\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims": {
      "get": {
        "tags": [
          "eSIMs"
        ],
        "summary": "List eSIMs",
        "description": "One row per ICCID (an eSIM that has been topped up appears once, with its\r\ncombined data). Rows do not include `usage` or `purchases`; use\r\n`GET /esims/{iccid}` for those. Pass `orderNo` to see the eSIMs of\r\none order instead.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by eSIM status.",
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/EsimStatus"
                }
              ]
            }
          },
          {
            "name": "iccid",
            "in": "query",
            "description": "Filter by (partial) ICCID.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orderNo",
            "in": "query",
            "description": "Only eSIMs issued by this order (its `orderNo`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "countryCode",
            "in": "query",
            "description": "Only eSIMs whose plan covers this ISO 3166-1 alpha-2 country.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdFrom",
            "in": "query",
            "description": "Only eSIMs created on or after this date (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "createdTo",
            "in": "query",
            "description": "Only eSIMs created on or before this date (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedListOfEsim"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/esims\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/esims\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}": {
      "get": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Get an eSIM",
        "description": "Full view including live `usage` and the purchase history. Install details are a separate call, `GET /esims/{iccid}/install`.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/install": {
      "get": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Get install details",
        "description": "Everything the end customer needs to install the eSIM: the activation code,\r\nits SM-DP+ address and matching id, a QR code image and one-tap links for iOS\r\nand Android. The shape is identical for every eSIM, whichever network issued\r\nit, so one delivery template serves your whole catalogue. Available from the\r\nmoment the eSIM is issued (status `Active`) and for as long as it has a\r\nprofile; `400 InstallUnavailable` while it is still `Provisioning`\r\nand once it is `Revoked` or `Deactivated`.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EsimInstall"
                }
              }
            }
          },
          "400": {
            "description": "The eSIM has no installable profile right now (`InstallUnavailable`): not issued yet, or revoked/reclaimed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/install\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/install\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/install\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/install\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/install\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/usage": {
      "get": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Get live usage",
        "description": "Usage is read from the network and refreshed about once an hour, so it can\r\nlag real consumption by up to that much; polling more often returns the same\r\nfigures. When `live` is false the eSIM has not connected yet and the\r\nfigures are derived from the plan. Byte values are as reported by the\r\nnetwork; use `usedPercentage` for display.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EsimUsage"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/usage\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/usage\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/usage\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/usage\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/usage\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/top-up-plans": {
      "get": {
        "tags": [
          "eSIMs"
        ],
        "summary": "List top-up plans for an eSIM",
        "description": "Only plans compatible with this eSIM's network and destination are returned. Pass one of them to `POST /esims/{iccid}/top-up`.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedListOfPlan"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up-plans\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up-plans\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up-plans\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up-plans\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up-plans\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/top-up": {
      "post": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Top up an eSIM",
        "description": "Adds a plan to the existing ICCID and debits your wallet for its price. `planCode` must be one of the plans `GET /esims/{iccid}/top-up-plans` returns for this eSIM (otherwise `PlanNotFound`). The eSIM's `plan.dataGb` and `purchaseCount` reflect the combined total afterwards.\r\n            \r\nAlways send an `idempotencyKey`. If the same key is sent again for this\r\neSIM, nothing is bought or charged: the eSIM is returned as it stands, with\r\nthe header `Idempotent-Replayed: true` — this is how you retry safely\r\nafter a timeout. The other fields of a replayed request are ignored. A key\r\nalready used for a different purchase (an order, or another eSIM's top-up) is\r\nrejected with `409 IdempotencyKeyReused`. If the first request with that\r\nkey is still being processed when the second arrives, the second gets\r\n`409 IdempotencyKeyInProgress`: nothing is bought, and the same key can\r\nbe retried a few seconds later.\r\n            \r\nYour wallet is debited before the network is asked to add the plan. If the\r\nnetwork refuses, the call fails, the amount is returned at once (a\r\n`Debit` and a matching `Refund` appear in your wallet transactions)\r\nand the same `idempotencyKey` can be used again.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EsimTopUp"
                  }
                ],
                "description": "Add a plan to an existing eSIM.",
                "example": {
                  "planCode": "CKH693",
                  "idempotencyKey": "shop-topup-88213-1"
                }
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EsimTopUp"
                  }
                ],
                "description": "Add a plan to an existing eSIM.",
                "example": {
                  "planCode": "CKH693",
                  "idempotencyKey": "shop-topup-88213-1"
                }
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EsimTopUp"
                  }
                ],
                "description": "Add a plan to an existing eSIM.",
                "example": {
                  "planCode": "CKH693",
                  "idempotencyKey": "shop-topup-88213-1"
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "The eSIM after the top-up. With the header `Idempotent-Replayed: true` it is a replay: this `idempotencyKey` had already topped up this eSIM and nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "402": {
            "description": "Your wallet balance cannot cover the purchase (`InsufficientBalance`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The `idempotencyKey` was already used for a different purchase (`IdempotencyKeyReused`), another request with it is still being processed (`IdempotencyKeyInProgress`; retry shortly with the same key), or an earlier top-up with it was charged but never finished (`TopUpUnresolved`; do not retry, contact support).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X POST \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up\" \\\n  -H \"Authorization: {key}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"planCode\": \"CKH693\",\n  \"idempotencyKey\": \"shop-topup-88213-1\"\n}'"
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Post -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up\" `\n  -Headers @{ Authorization = \"{key}\" } `\n  -ContentType \"application/json\" `\n  -Body '{\n  \"planCode\": \"CKH693\",\n  \"idempotencyKey\": \"shop-topup-88213-1\"\n}'"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"{key}\",\n    \"Content-Type\": \"application/json\"\n  },\n  body: JSON.stringify({\n  \"planCode\": \"CKH693\",\n  \"idempotencyKey\": \"shop-topup-88213-1\"\n})\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"POST\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up\",\n    headers={\"Authorization\": \"{key}\"},\n    json={\n  \"planCode\": \"CKH693\",\n  \"idempotencyKey\": \"shop-topup-88213-1\"\n},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Post, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/top-up\");\nrequest.Content = new StringContent(\"\"\"{\"planCode\":\"CKH693\",\"idempotencyKey\":\"shop-topup-88213-1\"}\"\"\", Encoding.UTF8, \"application/json\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/suspend": {
      "post": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Suspend an eSIM",
        "description": "Pauses connectivity without cancelling the plan. Validity keeps running. Reverse with `POST /esims/{iccid}/unsuspend`. Only an eSIM whose status is `Installed` or `Active` can be suspended; any other status returns `400 OnlyActiveEsimsCanBeSuspended`. An eSIM that has not been installed yet cannot be suspended; revoke it instead if it must not be used.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X POST \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/suspend\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Post -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/suspend\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/suspend\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"POST\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/suspend\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Post, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/suspend\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/unsuspend": {
      "post": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Unsuspend an eSIM",
        "description": "Restores connectivity on an eSIM you suspended. The status returns to what the network reports; validity was never paused.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X POST \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/unsuspend\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Post -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/unsuspend\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/unsuspend\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"POST\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/unsuspend\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Post, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/unsuspend\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/revoke-eligibility": {
      "get": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Check whether revoking would be refunded",
        "description": "Read-only. Call it before `POST /esims/{iccid}/revoke` to tell your user whether the price comes back to the wallet.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevokeEligibility"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke-eligibility\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke-eligibility\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke-eligibility\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke-eligibility\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke-eligibility\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/revoke": {
      "post": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Revoke an eSIM",
        "description": "Permanently cancels the profile. The status becomes `Revoked` and\r\n`GET /esims/{iccid}/install` stops serving its details. If the eSIM was refundable at that\r\nmoment, its price is returned to your wallet as a `Refund` entry. This\r\ncannot be undone.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X POST \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Post -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"POST\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Post, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/revoke\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/esims/{iccid}/label": {
      "put": {
        "tags": [
          "eSIMs"
        ],
        "summary": "Set an eSIM's label",
        "description": "Free text for your own reference (an end-customer name, an internal order id). Shown in the Kelwa portal too.",
        "parameters": [
          {
            "name": "iccid",
            "in": "path",
            "description": "The eSIM's ICCID (`iccid` in any eSIM response).",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "8910300000059819483"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EsimLabel"
                  }
                ],
                "description": "Give the eSIM a label of your own.",
                "example": {
                  "label": "Customer 88213"
                }
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EsimLabel"
                  }
                ],
                "description": "Give the eSIM a label of your own.",
                "example": {
                  "label": "Customer 88213"
                }
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/EsimLabel"
                  }
                ],
                "description": "Give the eSIM a label of your own.",
                "example": {
                  "label": "Customer 88213"
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Esim"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X PUT \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/label\" \\\n  -H \"Authorization: {key}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"label\": \"Customer 88213\"\n}'"
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Put -Uri \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/label\" `\n  -Headers @{ Authorization = \"{key}\" } `\n  -ContentType \"application/json\" `\n  -Body '{\n  \"label\": \"Customer 88213\"\n}'"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/label\", {\n  method: \"PUT\",\n  headers: {\n    Authorization: \"{key}\",\n    \"Content-Type\": \"application/json\"\n  },\n  body: JSON.stringify({\n  \"label\": \"Customer 88213\"\n})\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"PUT\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/label\",\n    headers={\"Authorization\": \"{key}\"},\n    json={\n  \"label\": \"Customer 88213\"\n},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Put, \"https://api.kelwaesim.com/api/kelwa/v1/esims/8910300000059819483/label\");\nrequest.Content = new StringContent(\"\"\"{\"label\":\"Customer 88213\"}\"\"\", Encoding.UTF8, \"application/json\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/orders": {
      "post": {
        "tags": [
          "Orders"
        ],
        "summary": "Create an order",
        "description": "Debits your wallet for `quantity × plan price` and issues that many\r\neSIMs. The response is the order with every eSIM; take each eSIM's\r\n`iccid` to `GET /esims/{iccid}/install` for what the end customer\r\nneeds to install it.\r\n            \r\nAlways send an `idempotencyKey`. If the same key is sent again for your\r\naccount, the original order is returned with `200 OK` (a new order is\r\n`201 Created`) and the header `Idempotent-Replayed: true`, and\r\nnothing is charged — this is how you retry safely after a timeout. The other\r\nfields of a replayed request are ignored; the original order is what you get.\r\nIf the first request with that key is still being processed when the second\r\narrives, the second gets `409 IdempotencyKeyInProgress`: nothing is\r\nbought, and the same key can be retried a few seconds later.\r\n            \r\nIf an individual eSIM fails to issue, the order is returned as\r\n`PartiallyCompleted` or `Failed` and the failed eSIMs carry a\r\n`failureReason`. The wallet debit stands; Kelwa operations follow up on\r\nfailed eSIMs, so do not re-order the same quantity.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Order"
                  }
                ],
                "description": "An order to place: one plan, one or more times, debiting your wallet.",
                "example": {
                  "planCode": "CKH031",
                  "quantity": 1,
                  "idempotencyKey": "shop-order-88213"
                }
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Order"
                  }
                ],
                "description": "An order to place: one plan, one or more times, debiting your wallet.",
                "example": {
                  "planCode": "CKH031",
                  "quantity": 1,
                  "idempotencyKey": "shop-order-88213"
                }
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Order"
                  }
                ],
                "description": "An order to place: one plan, one or more times, debiting your wallet.",
                "example": {
                  "planCode": "CKH031",
                  "quantity": 1,
                  "idempotencyKey": "shop-order-88213"
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "201": {
            "description": "A new order was created and your wallet debited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderDetails"
                }
              }
            }
          },
          "200": {
            "description": "Replay: an order with this `idempotencyKey` already existed and is returned unchanged; nothing was charged. Also signalled by the `Idempotent-Replayed: true` header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderDetails"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "402": {
            "description": "Your wallet balance cannot cover the purchase (`InsufficientBalance`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another request with this `idempotencyKey` is still being processed (`IdempotencyKeyInProgress`). Nothing was bought; retry shortly with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X POST \"https://api.kelwaesim.com/api/kelwa/v1/orders\" \\\n  -H \"Authorization: {key}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"planCode\": \"CKH031\",\n  \"quantity\": 1,\n  \"idempotencyKey\": \"shop-order-88213\"\n}'"
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Post -Uri \"https://api.kelwaesim.com/api/kelwa/v1/orders\" `\n  -Headers @{ Authorization = \"{key}\" } `\n  -ContentType \"application/json\" `\n  -Body '{\n  \"planCode\": \"CKH031\",\n  \"quantity\": 1,\n  \"idempotencyKey\": \"shop-order-88213\"\n}'"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/orders\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"{key}\",\n    \"Content-Type\": \"application/json\"\n  },\n  body: JSON.stringify({\n  \"planCode\": \"CKH031\",\n  \"quantity\": 1,\n  \"idempotencyKey\": \"shop-order-88213\"\n})\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"POST\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/orders\",\n    headers={\"Authorization\": \"{key}\"},\n    json={\n  \"planCode\": \"CKH031\",\n  \"quantity\": 1,\n  \"idempotencyKey\": \"shop-order-88213\"\n},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Post, \"https://api.kelwaesim.com/api/kelwa/v1/orders\");\nrequest.Content = new StringContent(\"\"\"{\"planCode\":\"CKH031\",\"quantity\":1,\"idempotencyKey\":\"shop-order-88213\"}\"\"\", Encoding.UTF8, \"application/json\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      },
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "List orders",
        "description": "Newest first, without the eSIMs. Use `GET /orders/{orderNo}` for an order's eSIMs and install details.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by order status.",
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/OrderStatus"
                }
              ]
            }
          },
          {
            "name": "orderNo",
            "in": "query",
            "description": "Filter by (partial) Kelwa order number.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdFrom",
            "in": "query",
            "description": "Only orders created on or after this date (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "createdTo",
            "in": "query",
            "description": "Only orders created on or before this date (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedListOfOrderSummary"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/orders\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/orders\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/orders\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/orders\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/orders\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/orders/{orderNo}": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "Get an order",
        "description": "The order with every eSIM it issued — the same shape `POST /orders` returned at purchase time.",
        "parameters": [
          {
            "name": "orderNo",
            "in": "path",
            "description": "Kelwa order number (`orderNo` in any order response), e.g. `KO-20260101120000-AB12CD`.",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "KO-20261104083545-1F9C2A"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/orders/KO-20261104083545-1F9C2A\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/orders/KO-20261104083545-1F9C2A\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/orders/KO-20261104083545-1F9C2A\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/orders/KO-20261104083545-1F9C2A\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/orders/KO-20261104083545-1F9C2A\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/ping": {
      "get": {
        "tags": [
          "Ping"
        ],
        "summary": "Verify your API key",
        "description": "Returns the account and key the credential resolves to. Use it to validate a new key before wiring anything else.",
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ping"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/ping\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/ping\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/ping\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/ping\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/ping\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/plans": {
      "get": {
        "tags": [
          "Plans"
        ],
        "summary": "List plans for a destination",
        "description": "With no filter, every sellable plan is returned: country plans first (by\r\ncountry), then regional plans. Filter by `countryCode` or\r\n`regionCode` (not both) for one destination.\r\nPrices are your account's current prices and can change; read them at order\r\ntime rather than caching them long-term.",
        "parameters": [
          {
            "name": "countryCode",
            "in": "query",
            "description": "ISO 3166-1 alpha-2, e.g. `AE` (`code` of a Country destination).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "regionCode",
            "in": "query",
            "description": "Kelwa region code, e.g. `EU-33` (`code` of a Region destination).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedListOfPlan"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the action is not possible in the resource's current state. `error.code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/plans\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/plans\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/plans\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/plans\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/plans\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/plans/{planCode}": {
      "get": {
        "tags": [
          "Plans"
        ],
        "summary": "Get a plan",
        "description": "One plan with its current price for your account. Returns 404 if the plan is not sellable to you.",
        "parameters": [
          {
            "name": "planCode",
            "in": "path",
            "description": "Kelwa plan code (`name` in any plan response), e.g. `CKH031`.",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "CKH031"
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "404": {
            "description": "No such resource in your account (`PlanNotFound`, `OrderNotFound`, `EsimNotFound`, ...).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/plans/CKH031\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/plans/CKH031\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/plans/CKH031\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/plans/CKH031\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/plans/CKH031\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/wallet/balance": {
      "get": {
        "tags": [
          "Wallet"
        ],
        "summary": "Get the wallet balance",
        "description": "The balance orders are paid from. Funding is done in the Kelwa portal (card or bank transfer), not through the API.",
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletBalance"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/wallet/balance\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/wallet/balance\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/wallet/balance\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/wallet/balance\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/wallet/balance\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    },
    "/api/kelwa/v1/wallet/transactions": {
      "get": {
        "tags": [
          "Wallet"
        ],
        "summary": "List wallet transactions",
        "description": "Newest first. Debits reference the order number they paid for; refunds reference the eSIM they returned money for.",
        "parameters": [
          {
            "name": "createdFrom",
            "in": "query",
            "description": "Only entries created on or after this date (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "createdTo",
            "in": "query",
            "description": "Only entries created on or before this date (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number (default 1).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing, malformed, revoked or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded; wait the number of seconds in `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedListOfWalletTransaction"
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKey": [ ]
          }
        ],
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl -X GET \"https://api.kelwaesim.com/api/kelwa/v1/wallet/transactions\" \\\n  -H \"Authorization: {key}\""
          },
          {
            "lang": "powershell",
            "label": "PowerShell",
            "source": "Invoke-RestMethod -Method Get -Uri \"https://api.kelwaesim.com/api/kelwa/v1/wallet/transactions\" `\n  -Headers @{ Authorization = \"{key}\" }"
          },
          {
            "lang": "javascript",
            "label": "JavaScript",
            "source": "const response = await fetch(\"https://api.kelwaesim.com/api/kelwa/v1/wallet/transactions\", {\n  method: \"GET\",\n  headers: {\n    Authorization: \"{key}\"\n  }\n});\nconst data = await response.json();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.request(\n    \"GET\",\n    \"https://api.kelwaesim.com/api/kelwa/v1/wallet/transactions\",\n    headers={\"Authorization\": \"{key}\"},\n)\nresponse.raise_for_status()\ndata = response.json()"
          },
          {
            "lang": "csharp",
            "label": "C#",
            "source": "using var http = new HttpClient();\nhttp.DefaultRequestHeaders.Add(\"Authorization\", \"{key}\");\n\nvar request = new HttpRequestMessage(HttpMethod.Get, \"https://api.kelwaesim.com/api/kelwa/v1/wallet/transactions\");\n\nvar response = await http.SendAsync(request);\nresponse.EnsureSuccessStatusCode();\nvar json = await response.Content.ReadAsStringAsync();"
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ApiError": {
        "type": "object",
        "properties": {
          "error": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ApiErrorDetail"
              }
            ],
            "description": "The error. Branch on `error.code`."
          }
        },
        "additionalProperties": false,
        "description": "Error envelope returned by every non-2xx partner API response.",
        "example": {
          "error": {
            "code": "InsufficientBalance",
            "message": "Insufficient balance.",
            "requestId": "0HNPQ9Q87UNG7:00000001"
          }
        }
      },
      "ApiErrorDetail": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable, machine-readable error code (e.g. `InsufficientBalance`, `EsimNotFound`)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. Wording may change; branch on `code`, not on this."
          },
          "details": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Per-field validation messages, present only for request validation failures.",
            "nullable": true
          },
          "requestId": {
            "type": "string",
            "description": "Correlation id for this request — quote it when contacting Kelwa support.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "What went wrong, in a shape that is the same for every status code."
      },
      "CoverageCountry": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 code."
          },
          "name": {
            "type": "string",
            "description": "Country name in English."
          },
          "networks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Network"
            },
            "description": "Operators serving this country and the network types each offers. Empty when the catalogue has no operator data for the country."
          }
        },
        "additionalProperties": false,
        "description": "One covered country with the operators that serve it. The same object wherever\r\ncountries are listed: a region's coverage, a plan's and an eSIM's `coveredCountries`."
      },
      "Destination": {
        "type": "object",
        "properties": {
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DestinationType"
              }
            ],
            "description": "`Country` or `Region`."
          },
          "name": {
            "type": "string",
            "description": "Display name in English."
          },
          "code": {
            "type": "string",
            "description": "The destination's identifier: ISO 3166-1 alpha-2 code for a country (`AE`), Kelwa's short code for a region (`EU-33`). Pass it as `countryCode`/`regionCode` when listing plans.",
            "nullable": true
          },
          "regionName": {
            "type": "string",
            "description": "For a country, the name of the region it belongs to; null for a region.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A country or multi-country region that plans can be bought for.",
        "example": {
          "type": "Country",
          "name": "United Arab Emirates",
          "code": "AE",
          "regionName": "Middle East"
        }
      },
      "DestinationCoverage": {
        "type": "object",
        "properties": {
          "countries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CoverageCountry"
            },
            "description": "Every country a plan for this region works in."
          }
        },
        "additionalProperties": false,
        "description": "Which countries a region's plans cover, each with the operators serving it there.",
        "example": {
          "countries": [
            {
              "code": "MY",
              "name": "Malaysia",
              "networks": [
                {
                  "name": "Maxis",
                  "networkTypes": [
                    "4G",
                    "5G"
                  ]
                }
              ]
            },
            {
              "code": "SG",
              "name": "Singapore",
              "networks": [
                {
                  "name": "Singtel",
                  "networkTypes": [
                    "4G",
                    "5G"
                  ]
                }
              ]
            },
            {
              "code": "TH",
              "name": "Thailand",
              "networks": [
                {
                  "name": "AIS",
                  "networkTypes": [
                    "4G",
                    "5G"
                  ]
                }
              ]
            }
          ]
        }
      },
      "DestinationRef": {
        "type": "object",
        "properties": {
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DestinationType"
              }
            ],
            "description": "`Country` or `Region`.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Display name in English.",
            "nullable": true
          },
          "code": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 code for a country; Kelwa's short code for a region. The same value `GET /destinations` returns as `code`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Compact reference to a destination, used wherever another object points at one\r\n(plans, eSIMs, orders, wallet entries). Same shape everywhere."
      },
      "DestinationType": {
        "enum": [
          "Country",
          "Region"
        ],
        "type": "string",
        "description": "Kind of sellable destination."
      },
      "Esim": {
        "type": "object",
        "properties": {
          "iccid": {
            "type": "string",
            "description": "The eSIM's identifier — what every `/esims/{iccid}` endpoint takes. Null while `status` is Provisioning or Failed; such an eSIM is reachable only through its order until it is issued.",
            "nullable": true
          },
          "orderNo": {
            "type": "string",
            "description": "Order number (`KO-…`) of the latest purchase on this ICCID — what `GET /orders/{orderNo}` takes."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimStatus"
              }
            ],
            "description": "Current lifecycle state (see Statuses in the guide)."
          },
          "failureReason": {
            "type": "string",
            "description": "Why provisioning failed; only set when `status` is Failed.",
            "nullable": true
          },
          "label": {
            "type": "string",
            "description": "Your free-text label (see `PUT /esims/{iccid}/label`).",
            "nullable": true
          },
          "plan": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimPlan"
              }
            ],
            "description": "The plan on the eSIM, combined across the original purchase and any top-ups."
          },
          "destination": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DestinationRef"
              }
            ],
            "description": "Primary covered country.",
            "nullable": true
          },
          "coveredCountries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CoverageCountry"
            },
            "description": "Every country the eSIM works in, each with the operators serving it there and the network types they offer. Same shape as a plan's `coveredCountries`."
          },
          "validity": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimValidity"
              }
            ],
            "description": "Expiry and the timestamps of installation and first use."
          },
          "usage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimUsage"
              }
            ],
            "description": "Live usage. Only populated by the single-eSIM endpoints; null in lists.",
            "nullable": true,
            "example": {
              "live": true,
              "totalBytes": 3221225472,
              "usedBytes": 262144000,
              "remainingBytes": 2959081472,
              "usedPercentage": 8.1
            }
          },
          "purchaseCount": {
            "type": "integer",
            "description": "How many purchases (original + top-ups) make up this eSIM.",
            "format": "int32"
          },
          "purchases": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EsimPurchase"
            },
            "description": "Purchase history, oldest first. Only populated by `GET /esims/{iccid}`; null in lists.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "When the eSIM was ordered (UTC).",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "When Kelwa last recorded a change to it (UTC).",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One eSIM (one ICCID). After top-ups the same ICCID carries several purchases;\r\nthis object is the combined, current view of it.",
        "example": {
          "iccid": "8910300000059819483",
          "orderNo": "KO-20261104083545-1F9C2A",
          "status": "Active",
          "failureReason": null,
          "label": "Customer 88213",
          "plan": {
            "name": "CKH031",
            "description": "United Arab Emirates 3GB 30Days",
            "dataGb": 3,
            "dataBytes": 3221225472,
            "validityDays": 30,
            "price": 8,
            "currency": "USD"
          },
          "destination": {
            "type": "Country",
            "name": "United Arab Emirates",
            "code": "AE"
          },
          "coveredCountries": [
            {
              "code": "AE",
              "name": "United Arab Emirates",
              "networks": [
                {
                  "name": "Du",
                  "networkTypes": [
                    "3G",
                    "4G",
                    "5G"
                  ]
                }
              ]
            }
          ],
          "validity": {
            "expiresAt": "2026-12-04T08:35:49Z",
            "remainingDays": 30,
            "installedAt": null,
            "activatedAt": null
          },
          "usage": {
            "live": true,
            "totalBytes": 3221225472,
            "usedBytes": 262144000,
            "remainingBytes": 2959081472,
            "usedPercentage": 8.1
          },
          "purchaseCount": 1,
          "purchases": [
            {
              "planName": "CKH031",
              "description": "United Arab Emirates 3GB 30Days",
              "dataGb": 3,
              "dataBytes": 3221225472,
              "price": 8,
              "currency": "USD",
              "status": "Active",
              "transactionNo": "26110408350012",
              "createdAt": "2026-11-04T08:35:48Z"
            }
          ],
          "createdAt": "2026-11-04T08:35:48Z",
          "updatedAt": "2026-11-04T08:35:49Z"
        }
      },
      "EsimInstall": {
        "type": "object",
        "properties": {
          "iccid": {
            "type": "string",
            "description": "ICCID of the eSIM these details install."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimStatus"
              }
            ],
            "description": "The eSIM's current status at the time of the call."
          },
          "activationCode": {
            "type": "string",
            "description": "Full GSMA activation string, `LPA:1$<smdpAddress>$<matchingId>`. This is what a QR code encodes; render your own from it if you prefer."
          },
          "smdpAddress": {
            "type": "string",
            "description": "SM-DP+ server address, for devices that ask for the address and code separately.",
            "nullable": true
          },
          "matchingId": {
            "type": "string",
            "description": "Activation code (matching id) part, for the same manual-entry screens.",
            "nullable": true
          },
          "qrCode": {
            "type": "string",
            "description": "QR code of `activationCode` as a PNG `data:image/png;base64,…` URI. Use it directly as an image source or embed it in an email; it is about 2 KB."
          },
          "iosUniversalUrl": {
            "type": "string",
            "description": "One-tap install link for iOS 17.4 and later."
          },
          "androidUniversalUrl": {
            "type": "string",
            "description": "One-tap install link for Android."
          },
          "giftLinkUrl": {
            "type": "string",
            "description": "Kelwa gift link for handing this eSIM to an end customer. Null unless gift links are enabled on your account.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Everything an end customer needs to install an eSIM, returned by\r\n`GET /esims/{iccid}/install`. The same fields are always present for every\r\nissued eSIM, whichever network it was issued on.",
        "example": {
          "iccid": "8910300000059819483",
          "status": "Active",
          "activationCode": "LPA:1$rsp.example.net$7BF6988BDB72400D96FA371326D442A4",
          "smdpAddress": "rsp.example.net",
          "matchingId": "7BF6988BDB72400D96FA371326D442A4",
          "qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZoAAAGaAQAAAAAefbjOAAAC…",
          "iosUniversalUrl": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.net%247BF6988BDB72400D96FA371326D442A4",
          "androidUniversalUrl": "https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24rsp.example.net%247BF6988BDB72400D96FA371326D442A4",
          "giftLinkUrl": null
        }
      },
      "EsimLabel": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Free text, max 100 chars. Send an empty string to clear.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Give the eSIM a label of your own.",
        "example": {
          "label": "Customer 88213"
        }
      },
      "EsimPlan": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Plan code of the latest purchase."
          },
          "description": {
            "type": "string",
            "description": "Plan title of the latest purchase.",
            "nullable": true
          },
          "dataGb": {
            "type": "number",
            "description": "Total data across all purchases, in GB (decimal).",
            "format": "double"
          },
          "dataBytes": {
            "type": "integer",
            "description": "Total data across all purchases, in bytes (binary, 1 GB = 1073741824 B). Null for unlimited plans.",
            "format": "int64",
            "nullable": true
          },
          "validityDays": {
            "type": "integer",
            "description": "Validity in days of the latest purchase.",
            "format": "int32"
          },
          "price": {
            "type": "number",
            "description": "Price paid for the latest purchase.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `price`."
          }
        },
        "additionalProperties": false,
        "description": "Snapshot of the plan(s) on the eSIM. Data is the total across all purchases."
      },
      "EsimPurchase": {
        "type": "object",
        "properties": {
          "planName": {
            "type": "string",
            "description": "Plan code that was bought."
          },
          "description": {
            "type": "string",
            "description": "Plan title that was bought.",
            "nullable": true
          },
          "dataGb": {
            "type": "number",
            "description": "Data this purchase added, in GB (decimal).",
            "format": "double"
          },
          "dataBytes": {
            "type": "integer",
            "description": "Data this purchase added, in bytes (binary, 1 GB = 1073741824 B). Null for unlimited plans.",
            "format": "int64",
            "nullable": true
          },
          "price": {
            "type": "number",
            "description": "Price paid for this purchase.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `price`."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EsimStatus"
              }
            ],
            "description": "State of this purchase's data bundle."
          },
          "transactionNo": {
            "type": "string",
            "description": "Kelwa's reference for this purchase's data bundle. Webhook events about this bundle carry the same value as `data.transactionNo`, so you can tell which purchase on the ICCID an event belongs to. Null until the network has assigned it, which for some eSIMs is only at first use.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "When this purchase was made (UTC).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "One purchase on an eSIM: the original order or a later top-up."
      },
      "EsimStatus": {
        "enum": [
          "Unknown",
          "Provisioning",
          "Active",
          "Installed",
          "NotInstalled",
          "Suspended",
          "Consumed",
          "Expired",
          "Revoked",
          "Deactivated",
          "Failed"
        ],
        "type": "string",
        "description": "Lifecycle state of an eSIM, in Kelwa's own vocabulary. This is the same status\r\nset the Kelwa portal shows, so what a partner sees in the API and what Kelwa\r\nsupport sees in the portal always agree."
      },
      "EsimTopUp": {
        "type": "object",
        "properties": {
          "planCode": {
            "type": "string",
            "description": "Code (`name`) of a plan returned by `GET /esims/{iccid}/top-up-plans`."
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Your own unique reference for this top-up (max 100 chars). Strongly\r\nrecommended: resending the same key returns the eSIM as it stands instead of\r\ncharging your wallet again, so a retried request can never double-buy. Keys\r\nare unique per account across orders and top-ups; use a new one for every\r\npurchase.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Add a plan to an existing eSIM.",
        "example": {
          "planCode": "CKH693",
          "idempotencyKey": "shop-topup-88213-1"
        }
      },
      "EsimUsage": {
        "type": "object",
        "properties": {
          "live": {
            "type": "boolean",
            "description": "True when these figures come from the network's meter; false when the eSIM has not connected yet and they are derived from the plan."
          },
          "totalBytes": {
            "type": "integer",
            "description": "Data allowance across all purchases, in bytes.",
            "format": "int64"
          },
          "usedBytes": {
            "type": "integer",
            "description": "Bytes consumed so far. Null when the live usage lookup is temporarily unavailable.",
            "format": "int64",
            "nullable": true
          },
          "remainingBytes": {
            "type": "integer",
            "description": "Bytes left. Null when the live usage lookup is temporarily unavailable.",
            "format": "int64",
            "nullable": true
          },
          "usedPercentage": {
            "type": "number",
            "description": "Share of the allowance used, 0–100. Null when the live usage lookup is temporarily unavailable.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Data usage in bytes. When `live` is true the figures are the network's own meter reading. Before the eSIM has ever connected the network has no meter for it yet: `live` is false, `totalBytes` equals `plan.dataBytes` and nothing has been used. Use `usedPercentage` for display.",
        "example": {
          "live": true,
          "totalBytes": 3221225472,
          "usedBytes": 262144000,
          "remainingBytes": 2959081472,
          "usedPercentage": 8.1
        }
      },
      "EsimValidity": {
        "type": "object",
        "properties": {
          "expiresAt": {
            "type": "string",
            "description": "Hard end of the plan, anchored at purchase time.",
            "format": "date-time",
            "nullable": true
          },
          "remainingDays": {
            "type": "integer",
            "description": "Days of validity remaining. Counts down from first use; the full validity until then.",
            "format": "int32"
          },
          "installedAt": {
            "type": "string",
            "description": "When the profile was downloaded to a device.",
            "format": "date-time",
            "nullable": true
          },
          "activatedAt": {
            "type": "string",
            "description": "When the eSIM first connected to a network.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "When the eSIM expires and when it was installed and first used."
      },
      "Network": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Operator name as shown to the end customer."
          },
          "networkTypes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Network types offered, e.g. `4G`, `5G`."
          }
        },
        "additionalProperties": false,
        "description": "One mobile operator in one country."
      },
      "Order": {
        "type": "object",
        "properties": {
          "planCode": {
            "type": "string",
            "description": "Code (`name`) of the plan to buy, from `GET /plans`, e.g. `CKH031`."
          },
          "quantity": {
            "type": "integer",
            "description": "Number of eSIMs to issue for this plan (1–50). Defaults to 1.",
            "format": "int32"
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Your own unique reference for this purchase (max 100 chars). Strongly\r\nrecommended: resending the same key returns the original order instead of\r\ncharging your wallet again, so a retried request can never double-buy.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An order to place: one plan, one or more times, debiting your wallet.",
        "example": {
          "planCode": "CKH031",
          "quantity": 1,
          "idempotencyKey": "shop-order-88213"
        }
      },
      "OrderDetails": {
        "type": "object",
        "properties": {
          "orderNo": {
            "type": "string",
            "description": "Kelwa order number, e.g. `KO-20260101120000-AB12CD` — the order's identifier, what `GET /orders/{orderNo}` takes. Shown in the portal and on wallet entries."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OrderStatus"
              }
            ],
            "description": "Outcome of the order, derived from its eSIMs."
          },
          "totalAmount": {
            "type": "number",
            "description": "Total debited from your wallet for this order.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `totalAmount`."
          },
          "esimCount": {
            "type": "integer",
            "description": "Number of eSIMs requested (issued or failed).",
            "format": "int32"
          },
          "createdAt": {
            "type": "string",
            "description": "When the order was placed (UTC).",
            "format": "date-time"
          },
          "esims": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Esim"
            },
            "description": "Every eSIM in the order. Fetch install details per eSIM from `GET /esims/{iccid}/install`."
          }
        },
        "additionalProperties": false,
        "description": "A placed order and the eSIMs it issued.",
        "example": {
          "orderNo": "KO-20261104083545-1F9C2A",
          "status": "Completed",
          "totalAmount": 8,
          "currency": "USD",
          "esimCount": 1,
          "createdAt": "2026-11-04T08:35:45Z",
          "esims": [
            {
              "iccid": "8910300000059819483",
              "orderNo": "KO-20261104083545-1F9C2A",
              "status": "Active",
              "failureReason": null,
              "label": "Customer 88213",
              "plan": {
                "name": "CKH031",
                "description": "United Arab Emirates 3GB 30Days",
                "dataGb": 3,
                "dataBytes": 3221225472,
                "validityDays": 30,
                "price": 8,
                "currency": "USD"
              },
              "destination": {
                "type": "Country",
                "name": "United Arab Emirates",
                "code": "AE"
              },
              "coveredCountries": [
                {
                  "code": "AE",
                  "name": "United Arab Emirates",
                  "networks": [
                    {
                      "name": "Du",
                      "networkTypes": [
                        "3G",
                        "4G",
                        "5G"
                      ]
                    }
                  ]
                }
              ],
              "validity": {
                "expiresAt": "2026-12-04T08:35:49Z",
                "remainingDays": 30,
                "installedAt": null,
                "activatedAt": null
              },
              "usage": {
                "live": true,
                "totalBytes": 3221225472,
                "usedBytes": 262144000,
                "remainingBytes": 2959081472,
                "usedPercentage": 8.1
              },
              "purchaseCount": 1,
              "purchases": [
                {
                  "planName": "CKH031",
                  "description": "United Arab Emirates 3GB 30Days",
                  "dataGb": 3,
                  "dataBytes": 3221225472,
                  "price": 8,
                  "currency": "USD",
                  "status": "Active",
                  "transactionNo": "26110408350012",
                  "createdAt": "2026-11-04T08:35:48Z"
                }
              ],
              "createdAt": "2026-11-04T08:35:48Z",
              "updatedAt": "2026-11-04T08:35:49Z"
            }
          ]
        }
      },
      "OrderStatus": {
        "enum": [
          "Completed",
          "PartiallyCompleted",
          "Failed"
        ],
        "type": "string",
        "description": "Outcome of an order, derived from its eSIMs."
      },
      "OrderSummary": {
        "type": "object",
        "properties": {
          "orderNo": {
            "type": "string",
            "description": "Kelwa order number, e.g. `KO-20260101120000-AB12CD` — what `GET /orders/{orderNo}` takes."
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OrderStatus"
              }
            ],
            "description": "Outcome of the order, derived from its eSIMs."
          },
          "totalAmount": {
            "type": "number",
            "description": "Total debited from your wallet for this order.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `totalAmount`."
          },
          "esimCount": {
            "type": "integer",
            "description": "Number of eSIMs requested (issued or failed).",
            "format": "int32"
          },
          "destination": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DestinationRef"
              }
            ],
            "description": "Primary destination of the order's plan.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "When the order was placed (UTC).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "One placed order as it appears in `GET /orders`; use `GET /orders/{orderNo}` for its eSIMs.",
        "example": {
          "orderNo": "KO-20261104083545-1F9C2A",
          "status": "Completed",
          "totalAmount": 8,
          "currency": "USD",
          "esimCount": 1,
          "destination": {
            "type": "Country",
            "name": "United Arab Emirates",
            "code": "AE"
          },
          "createdAt": "2026-11-04T08:35:45Z"
        }
      },
      "PagedListOfDestination": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Destination"
            },
            "description": "The items on this page."
          },
          "page": {
            "type": "integer",
            "description": "1-based page number that was returned.",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "description": "Page size that was applied.",
            "format": "int32"
          },
          "totalItems": {
            "type": "integer",
            "description": "Total number of items across all pages.",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages for this page size.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One page of a list endpoint."
      },
      "PagedListOfEsim": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Esim"
            },
            "description": "The items on this page."
          },
          "page": {
            "type": "integer",
            "description": "1-based page number that was returned.",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "description": "Page size that was applied.",
            "format": "int32"
          },
          "totalItems": {
            "type": "integer",
            "description": "Total number of items across all pages.",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages for this page size.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One page of a list endpoint."
      },
      "PagedListOfOrderSummary": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderSummary"
            },
            "description": "The items on this page."
          },
          "page": {
            "type": "integer",
            "description": "1-based page number that was returned.",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "description": "Page size that was applied.",
            "format": "int32"
          },
          "totalItems": {
            "type": "integer",
            "description": "Total number of items across all pages.",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages for this page size.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One page of a list endpoint."
      },
      "PagedListOfPlan": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Plan"
            },
            "description": "The items on this page."
          },
          "page": {
            "type": "integer",
            "description": "1-based page number that was returned.",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "description": "Page size that was applied.",
            "format": "int32"
          },
          "totalItems": {
            "type": "integer",
            "description": "Total number of items across all pages.",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages for this page size.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One page of a list endpoint."
      },
      "PagedListOfWalletTransaction": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WalletTransaction"
            },
            "description": "The items on this page."
          },
          "page": {
            "type": "integer",
            "description": "1-based page number that was returned.",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "description": "Page size that was applied.",
            "format": "int32"
          },
          "totalItems": {
            "type": "integer",
            "description": "Total number of items across all pages.",
            "format": "int32"
          },
          "totalPages": {
            "type": "integer",
            "description": "Total number of pages for this page size.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One page of a list endpoint."
      },
      "Ping": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Always `ok`."
          },
          "accountId": {
            "type": "integer",
            "description": "Kelwa account the key belongs to.",
            "format": "int64"
          },
          "keyPrefix": {
            "type": "string",
            "description": "First characters of the key you authenticated with, as listed in the portal.",
            "nullable": true
          },
          "timestamp": {
            "type": "string",
            "description": "Server time of this response (UTC).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Proof that the key works and which account it belongs to."
      },
      "Plan": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Kelwa plan code, e.g. `CKH031` — what you pass as `planCode` when creating an order or topping up. Stable across price changes."
          },
          "description": {
            "type": "string",
            "description": "Human-readable plan title, e.g. `United Arab Emirates 3GB 30Days`.",
            "nullable": true
          },
          "dataGb": {
            "type": "number",
            "description": "Included data in GB (decimal, 1 GB = 1000 MB). Ignore when `unlimited` is true.",
            "format": "double"
          },
          "dataBytes": {
            "type": "integer",
            "description": "Included data in bytes (binary, 1 GB = 1073741824 B), the unit the network meters in. Null when `unlimited` is true.",
            "format": "int64",
            "nullable": true
          },
          "unlimited": {
            "type": "boolean",
            "description": "True for plans with no data cap; `dataGb` is then meaningless."
          },
          "validityDays": {
            "type": "integer",
            "description": "Validity in days, counted from first use.",
            "format": "int32"
          },
          "price": {
            "type": "number",
            "description": "Price charged to your wallet per eSIM.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `price` (your wallet currency)."
          },
          "destination": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DestinationRef"
              }
            ],
            "description": "The country or region the plan is sold for.",
            "nullable": true
          },
          "coveredCountries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CoverageCountry"
            },
            "description": "Every country the plan works in, each with the operators serving it there and the network types they offer. For a single-country plan this is one entry."
          }
        },
        "additionalProperties": false,
        "description": "A sellable plan and its current price for your account.",
        "example": {
          "name": "CKH031",
          "description": "United Arab Emirates 3GB 30Days",
          "dataGb": 3,
          "dataBytes": 3221225472,
          "unlimited": false,
          "validityDays": 30,
          "price": 8,
          "currency": "USD",
          "destination": {
            "type": "Country",
            "name": "United Arab Emirates",
            "code": "AE"
          },
          "coveredCountries": [
            {
              "code": "AE",
              "name": "United Arab Emirates",
              "networks": [
                {
                  "name": "Du",
                  "networkTypes": [
                    "3G",
                    "4G",
                    "5G"
                  ]
                }
              ]
            }
          ]
        }
      },
      "RevokeEligibility": {
        "type": "object",
        "properties": {
          "iccid": {
            "type": "string",
            "description": "ICCID of the eSIM that was checked."
          },
          "refundable": {
            "type": "boolean",
            "description": "True if revoking now returns the price to your wallet as a `Refund` entry."
          }
        },
        "additionalProperties": false,
        "description": "Whether revoking this eSIM right now would return its price to your wallet.",
        "example": {
          "iccid": "8910300000059819483",
          "refundable": true
        }
      },
      "WalletBalance": {
        "type": "object",
        "properties": {
          "balance": {
            "type": "number",
            "description": "Current spendable balance.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `balance` (your wallet currency)."
          },
          "retrievedAt": {
            "type": "string",
            "description": "When the balance was read (UTC).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Your prepaid balance at the moment of the call.",
        "example": {
          "balance": 1250.5,
          "currency": "USD",
          "retrievedAt": "2026-11-04T08:35:45Z"
        }
      },
      "WalletTransaction": {
        "type": "object",
        "properties": {
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WalletTransactionType"
              }
            ],
            "description": "Credit (funds added), Debit (an order or top-up) or Refund (money returned)."
          },
          "amount": {
            "type": "number",
            "description": "Always positive; `type` gives the direction.",
            "format": "double"
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code of `amount` and `balanceAfter`."
          },
          "balanceAfter": {
            "type": "number",
            "description": "Wallet balance once this entry was applied.",
            "format": "double"
          },
          "reference": {
            "type": "string",
            "description": "Order number (Debit), top-up order number, or payment reference (Credit).",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text note added by Kelwa, e.g. the reason for a manual credit.",
            "nullable": true
          },
          "iccid": {
            "type": "string",
            "description": "ICCID of the eSIM this entry is tied to, when it concerns one specific eSIM.",
            "nullable": true
          },
          "destination": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DestinationRef"
              }
            ],
            "description": "Primary destination of the plan the entry paid for.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "When the entry was recorded (UTC).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "One wallet ledger entry.",
        "example": {
          "type": "Debit",
          "amount": 8,
          "currency": "USD",
          "balanceAfter": 1242.5,
          "reference": "KO-20261104083545-1F9C2A",
          "notes": "Kelwa order KO-20261104083545-1F9C2A",
          "iccid": null,
          "destination": {
            "type": "Country",
            "name": "United Arab Emirates",
            "code": "AE"
          },
          "createdAt": "2026-11-04T08:35:45Z"
        }
      },
      "WalletTransactionType": {
        "enum": [
          "Credit",
          "Debit",
          "Refund"
        ],
        "type": "string",
        "description": "Direction of a wallet ledger entry."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "Your partner API key, sent as the Authorization header value — generated in the Kelwa portal's Developer screen, revocable there at any time.",
        "name": "Authorization",
        "in": "header"
      }
    }
  },
  "tags": [
    {
      "name": "Ping",
      "description": "Verify a key and reach the API."
    },
    {
      "name": "Destinations",
      "description": "Countries and regions that plans can be bought for."
    },
    {
      "name": "Plans",
      "description": "What you can sell, and your price for it."
    },
    {
      "name": "Orders",
      "description": "Buy plans from your wallet and review past orders."
    },
    {
      "name": "eSIMs",
      "description": "Issued eSIMs: status, install details, usage and lifecycle actions."
    },
    {
      "name": "Wallet",
      "description": "Your prepaid balance and ledger."
    }
  ]
}