{"openapi":"3.1.0","info":{"title":"doctorSIM API","description":"Mobile top-ups, digital gift cards, and travel eSIM plans for 200+ countries via 1,600+ carriers.\nThis API powers the doctorSIM Agentic Commerce Platform, enabling AI agents\nand developers to integrate telecom commerce into their applications.\n\n## Authentication\n\nUse **either** API keys or OAuth 2.0 access tokens (RS256 JWT).\n\n### API keys (long-lived)\n\n```\nAuthorization: Bearer {api_id}:{api_secret}\n```\n\n- Sandbox keys use the `test_` prefix (no real delivery).\n- Production keys use the `live_` prefix (credits deducted).\n\nCreate keys in Mi Cuenta → API or apply at\n[doctorsim.com/en/signup_pro.html](https://doctorsim.com/en/signup_pro.html).\n\n### OAuth 2.0 (recommended for agents)\n\nDiscovery and agent guide:\n\n- [OAuth Authorization Server](https://www.doctorsim.com/.well-known/oauth-authorization-server)\n- [Protected resource metadata](https://api.doctorsim.com/.well-known/oauth-protected-resource/v2)\n- [auth.md](https://www.doctorsim.com/auth.md)\n\nAuthorization Code + PKCE (interactive PRO login):\n\n1. Register a client: `POST https://www.doctorsim.com/oauth/register`\n2. Redirect user to `https://www.doctorsim.com/oauth/authorize`\n3. Exchange code at `https://www.doctorsim.com/oauth/token`\n\nUse the access token as a standard Bearer JWT (no `api_id:api_secret` colon form):\n\n```\nAuthorization: Bearer {access_token}\n```\n\n**Scopes:** `orders:read`, `orders:write`, `balance:read`, `webhooks:read`, `webhooks:write`,\nplus the environment selector `sandbox`.\n\n| Task | Scopes |\n|------|--------|\n| Catalog, carrier lookup, preview | none beyond a valid token (`orders:read` recommended) |\n| Create orders (`POST /orders`) | `orders:write` |\n| Order status / list / lookup | `orders:read` |\n| Balance and balance history | `balance:read` (any PRO account) |\n| Webhooks | `webhooks:read` / `webhooks:write` (PRO account after API approval) |\n| Pay on a hosted checkout link | `orders:write` (regular accounts) — response field `payment_link` |\n\nA regular account is granted `orders:read`, `orders:write`, and\n`payment_link`, plus identity scopes (`openid`, `profile`) and the `sandbox`\nselector when requested. A PRO account before API approval is granted\n`orders:read`, `balance:read`, and `stats:read` (plus identity and `sandbox`\nwhen requested). `orders:write`, `webhooks:read`, `webhooks:write`, and\n`payment_link` on a PRO account are granted only after API approval and are\notherwise dropped at authorize, token, and refresh. Endpoints that need a\nmissing scope return HTTP 403 `INSUFFICIENT_SCOPE`.\n\nDefault grant when `scope` is omitted (approved PRO): `openid orders:read orders:write balance:read`.\nDefault grant when `scope` is omitted (PRO before approval): `openid orders:read balance:read`.\nDefault grant when `scope` is omitted (regular): `openid orders:read orders:write`.\n\n**Sandbox with OAuth:** add `sandbox` to the requested `scope`\n(e.g. `scope=openid orders:read orders:write balance:read sandbox`) or pass\n`environment=sandbox` on `/oauth/authorize`. The consent screen shows\n\"Sandbox mode\", and access + refreshed tokens carry `env: sandbox`. Without it,\ntokens are `env: production`. To switch environment, authorize again.\n\nJWT `aud` is the API resource identifier (`https://api.doctorsim.com/v2`).\nThe `env` claim is `sandbox` or `production`.\n\n**Order visibility across credentials:** orders belong to the account, not to the\ncredential. Any credential of the same account **and the same environment** can read\nthem: a sandbox OAuth token reads orders created with a `test_` key (and vice versa);\na production OAuth token reads orders created with a `live_` key. Sandbox credentials\nnever read production orders (`403 SANDBOX_READ_DENIED`) and production credentials\nnever see `sandbox_*` orders. Idempotency replay on `POST /orders` is per credential;\n`GET /orders/lookup` resolves a key across the account's credentials.\n\n## Rate Limits\n| Tier | Requests/minute |\n|------|-----------------|\n| Free | 50 |\n| Pro | 500 |\n| Enterprise | 5,000 |\n\nRate limit status is returned in response headers:\n- `X-RateLimit-Limit` - max requests per minute\n- `X-RateLimit-Remaining` - remaining requests in current window\n- `X-RateLimit-Reset` - Unix timestamp when the window resets\n\n## Environments\n\nAll API traffic uses **`https://api.doctorsim.com/v2`**. There is no separate staging hostname for agents or integrators.\n\n| Mode | How |\n|------|-----|\n| Sandbox | API keys with `test_` prefix, or OAuth access token with `env: sandbox` (scope `sandbox`) — no charges, no delivery |\n| Production | API keys with `live_` prefix, or OAuth access token with `env: production` (credits deducted) |\n\n### Sandbox order lifecycle\n\n1. `POST /orders` → `201` with `order_id` = `sandbox_<16 hex>`, `status: processing`,\n   `credits_used: \"0.00\"`, `simulated_credits_used` (gross a live key would debit =\n   sandbox preview `total_cost_eur`, incl. service fee and SMS fee).\n2. `GET /orders/{order_id}` → `processing` for ~3 seconds, then the simulated terminal\n   state. Default `fulfilled`. Pick another outcome with body `sandbox_outcome`\n   (`failed`, `cancelled`, `refunded`) or a top-up phone ending in `0001` (failed),\n   `0002` (cancelled), `0003` (refunded).\n3. Webhooks (if registered): `order.created`, then `order.fulfilled` / `order.failed`.\n4. `GET /orders`, `GET /balance` (virtual, `simulated: true`) and `GET /balance/history`\n   return sandbox-scoped data for sandbox credentials.\n\n**OAuth issuer:** `https://www.doctorsim.com` (register, authorize, token, userinfo).\n\n## Machine Payments (MPP) — pay-per-call\n\nFor headless agents that pay per API call with Stripe (no OAuth/API key), use the\nseparate MPP surface (gated by `agentic_mpp` on the API host):\n\n- Discovery: `GET /v2/mpp/openapi.json`\n- Manifest: `GET /.well-known/mpp.json`\n- Preview: `POST /v2/mpp/orders/preview` (free)\n- Order: `POST /v2/mpp/orders` (HTTP 402 challenge → `Authorization: Payment` with Stripe SPT)\n\nThe MPP (and x402) charge for a top-up / gift card is the gross `pricing.total_cost_eur`\n(product cost + service fee + SMS fee when requested), e.g. 0.98 + 0.99 = 197 cents. The\npreview `amount_cents`, the 402 challenge, `x-payment-info` and the Stripe charge all use that\namount. A credential minted for a different amount gets `402 payment-insufficient` before any\ncharge — request a new challenge.\n\nOAuth and API-key clients continue using `POST /v2/orders` with Bearer auth — unchanged.\nSee [stripe-directory-runbook.md](stripe-directory-runbook.md) for enable checklist.\n\n## Media URLs\n\nCountry flags, operator logos, and other catalog images in API responses are **always absolute HTTPS URLs on `www.doctorsim.com`**, not on legacy CDN hosts such as `img1.doctorsim.com`.\n\n| Asset | URL pattern |\n|-------|-------------|\n| Country flag | `https://www.doctorsim.com/img/country_flags/vectored/{iso_short_code}.svg` |\n| Operator logo | `https://www.doctorsim.com/img/oper/{filename}` |\n\nUse the `img` / `image` fields from the API as-is; do not rewrite hosts or paths.\n\n## Response Format\nAll responses follow a consistent envelope:\n```json\n{\n  \"success\": true,\n  \"data\": { ... },\n  \"meta\": {\n    \"request_id\": \"req_abc123...\",\n    \"timestamp\": \"2026-03-24T12:00:00Z\"\n  }\n}\n```\n\nEvery `/v2` response uses this envelope — including unknown routes and malformed\nids (`404 NOT_FOUND`). Error responses include an `error` object with a machine\n`code`, human `message`, and optional `next_step` hint for agents:\n```json\n{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"INVALID_PHONE_NUMBER\",\n    \"message\": \"The phone number format is invalid.\",\n    \"next_step\": \"Retry with E.164 (e.g. +522221231231) or call lookup_carrier.\"\n  },\n  \"meta\": { ... }\n}\n```\n\n## Discovery on www (same spec)\n\nAuditors and browsers that hit `doctorsim.com` (not the API host) can load this\ncontract at `https://www.doctorsim.com/openapi.json` or\n`https://www.doctorsim.com/api/openapi.yaml`. Those URLs alias this file.\nAll REST calls still go to `https://api.doctorsim.com/v2`.\n\nMachine-payment discovery for auditors that fetch this www document lives on\n`/mpp/orders*` and `/x402/orders*`. Those paths alias the live rails\n`https://api.doctorsim.com/v2/mpp` and `https://api.doctorsim.com/v2/x402`.\nThey do not retarget `POST /orders` and they do not change HTTP 402 behavior.\n","version":"2.0.0","x-guidance":"Machine payments are separate from this REST credit API. MPP (Stripe) paid orders are POST https://api.doctorsim.com/v2/mpp/orders with vertical aliases /topup, /esim, and /giftcard. x402 paid orders are POST https://api.doctorsim.com/v2/x402/orders with the same aliases. Unpaid calls are answered on the API host. Paths /mpp/orders* and /x402/orders* in this file are discovery aliases of those rails.","contact":{"name":"doctorSIM API Support","email":"info@doctorsim.com","url":"https://www.doctorsim.com/api-docs/"},"x-service-info":{"categories":["telecom","mobile-topup","gift-cards","travel-esim","payments"],"docs":{"homepage":"https://www.doctorsim.com/en-us/developers-agents.html","openapi_json":"https://www.doctorsim.com/openapi.json","explorer":"https://www.doctorsim.com/api-docs/"}},"license":{"name":"doctorSIM API Terms of Use","url":"https://www.doctorsim.com/legal_specific.html#agentic-api-terms"},"x-logo":{"url":"https://www.doctorsim.com/apple-touch-icon-512x512.png"}},"servers":[{"url":"https://api.doctorsim.com/v2","description":"doctorSIM API v2 (sandbox or production per API key prefix or OAuth env claim)"}],"security":[{"BearerAuth":[]},{"OAuthBearerAuth":[]}],"x-service-info":{"categories":["telecom","mobile-topup","gift-cards","travel-esim","payments"],"docs":{"homepage":"https://www.doctorsim.com/en-us/developers-agents.html","apiReference":"https://www.doctorsim.com/api-docs/","llms":"https://www.doctorsim.com/agents/SKILL.md"}},"tags":[{"name":"Core — Orders","description":"Shared order spine for every vertical — preview, create, history (`GET /orders?type=topup|giftcard|esim`), and status. Travel eSIM checkout currently uses the `/esim/orders*` routes under Travel eSIM; history and status are shared."},{"name":"Core — Balance","description":"Prepaid PRO credit balance. One balance funds top-up, gift card, and eSIM credit checkouts."},{"name":"Core — Webhooks","description":"Order event subscriptions for every vertical. Each payload carries the order `type`."},{"name":"Core — Status & utilities","description":"Discovery root, health check, and IP geolocation. No product logic."},{"name":"Top-up","description":"Mobile airtime, bundles, and data catalog — carrier lookup, countries, operators, service types, rates, promotions. Order through Core `/orders` with `price_token` + `phone`."},{"name":"Gift cards","description":"Digital gift card catalog (non-mobile brands) — same countries / brand search / rates surface as top-up. Order through Core `/orders` with `price_token` (no phone); fulfilled orders return redemption fields."},{"name":"Travel eSIM","description":"Travel eSIM destinations, plans, checkout (`/esim/orders*` aliases of Core `/orders`), line lookup by ICCID, and hosted QR."},{"name":"Native App Auth","description":"First-party doctorSIM iOS/Android app only (HS256 native JWT). Not for API-key or OAuth agent clients.\nConfirm / set / change password so the app can finish activation and reset in-app instead of a webview.\n"}],"paths":{"/":{"get":{"operationId":"getApiRoot","x-vertical":"core","summary":"API v2 root (discovery links)","description":"Public entry point for the v2 API. Returns stable links to health check, OpenAPI spec,\ninteractive explorer, RFC 9727 API catalog, and OAuth discovery documents.\nDoes not require authentication.\n","tags":["Core — Status & utilities"],"security":[],"responses":{"200":{"description":"Discovery link map","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"name":"doctorSIM API v2","api_version":"2.0.0","resource":"https://api.doctorsim.com/v2","links":{"self":"https://api.doctorsim.com/v2/","status":"https://api.doctorsim.com/v2/status","openapi":"https://www.doctorsim.com/api-docs/openapi.yaml","service_doc":"https://www.doctorsim.com/api-docs/","api_catalog":"https://api.doctorsim.com/.well-known/api-catalog","auth_guide":"https://www.doctorsim.com/auth.md","oauth_authorization_server":"https://www.doctorsim.com/.well-known/oauth-authorization-server","oauth_protected_resource":"https://api.doctorsim.com/.well-known/oauth-protected-resource/v2"}},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}}}}},"/status":{"get":{"operationId":"getStatus","x-vertical":"core","summary":"API health check","description":"Unauthenticated liveness probe. Used by RFC 9727 api-catalog `status` relation\nand uptime monitoring. Does not require authentication.\n","tags":["Core — Status & utilities"],"security":[],"responses":{"200":{"description":"API is operational","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"status":"ok","api_version":"2.0.0","environment":"production"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}}}}},"/topup/carriers/lookup/{phone}":{"get":{"operationId":"topupCarrierLookup","x-vertical":"topup","summary":"Identify carrier from phone number","description":"**First step for mobile top-ups when the user provides a phone number.** Detects which\nmobile operator a number belongs to. Use E.164 format (e.g., +522221231231). Returns\n`id_operator`, `id_country`, and related metadata.\n\nRecommended agent flow after lookup:\n1. `GET /operators/{id}/service-types` — ask the user airtime, bundles, or data\n2. `GET /operators/{child_id}/rates?q=…` — search bundle/data descriptions\n3. `POST /orders` with `price_token` and `phone`\n","tags":["Top-up"],"parameters":[{"name":"phone","in":"path","required":true,"description":"Phone number in E.164 format","schema":{"type":"string"},"example":"+522212345678"}],"responses":{"200":{"description":"Carrier identified","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"description":"Invalid phone number format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"INVALID_PHONE_NUMBER","message":"The phone number format is invalid. Use E.164 format: +34612345678"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"404":{"description":"Carrier not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/topup/countries":{"get":{"operationId":"listTopupCountries","x-vertical":"topup","summary":"List top-up countries","description":"Countries with at least one active mobile operator (airtime, bundles, data, voice, utilities).\n`GET /countries` (un-prefixed alias) returns every country for any vertical.\n\nReturns all countries where doctorSIM offers mobile top-up or gift card services. `data` is keyed by locale (`es`, `en`, …); each value is an array of country objects with agent-friendly field names.\n\n`img` is an absolute URL on `https://www.doctorsim.com` (SVG country flag under `/img/country_flags/vectored/`). Legacy `img1.doctorsim.com` URLs are not used.\n\nGuest responses are origin-cacheable (`Cache-Control: public, max-age=60, stale-while-revalidate=300`, `Vary: X-App-Platform`, ETag). Authenticated/sandbox stay `private, no-store`.\n","tags":["Top-up"],"responses":{"200":{"description":"List of countries by locale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"en":[{"id_country":34,"iso_short_code":"es","name_en":"Spain","slug_en":"spain","continent":"EU","img":"https://www.doctorsim.com/img/country_flags/vectored/es.svg"},{"id_country":1144,"iso_short_code":"mx","name_en":"Mexico","slug_en":"mexico","continent":"NA","img":"https://www.doctorsim.com/img/country_flags/vectored/mx.svg"}]},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/topup/operators":{"get":{"operationId":"listTopupOperators","x-vertical":"topup","summary":"List mobile operators","description":"Returns operators keyed by operator ID. Public fields include `id_operator`, `operator_name`,\n`iso_short_code`, `img` (absolute URL on `https://www.doctorsim.com/img/oper/…`), and `topup_type`.\nFilter with `?country=ES` (ISO-2) or `?country=34` (`id_country`). Omitting `country` still\nreturns the full catalog. Guest responses are origin-cacheable\n(`Cache-Control: public, max-age=60, stale-while-revalidate=300`, `Vary: X-App-Platform`, ETag).\nAuthenticated, sandbox, and secure-PRO responses stay `private, no-store`.\n","tags":["Top-up"],"parameters":[{"name":"country","in":"query","description":"Filter by id_country (from /countries) or ISO-2 code (ES, es). Without this param the full catalog is returned.","schema":{"type":"string"},"example":"ES"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}}],"responses":{"200":{"description":"List of operators","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/topup/operators/{id}/service-types":{"get":{"operationId":"getTopupOperatorServiceTypes","x-vertical":"topup","summary":"Get service types for an operator","description":"Returns available service types for an operator (e.g., airtime/saldo, bundles, internet, data).\nThe default \"airtime\" type is represented by the parent operator ID, while other service types\nhave their own child operator IDs.\n","tags":["Top-up"],"parameters":[{"name":"id","in":"path","required":true,"description":"Operator ID","schema":{"type":"string"},"example":"5123"}],"responses":{"200":{"description":"List of service types","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/topup/operators/{id}/rates":{"get":{"operationId":"getTopupOperatorRates","x-vertical":"topup","summary":"Get pricing and denominations for an operator","description":"Returns a flat `rates` array with agent-friendly pricing. Each rate includes human-readable\nfields (`product_name`, `description` for bundle/data details) and a stable `token` (for example\n`mexico-telcel-20-mxn-0__2945583__35767699__1.00__EUR`) — pass that value as `price_token`\nin `POST /orders`.\n\nUse `q` to search bundle/data descriptions (e.g. `q=5GB whatsapp`). Combine with\n`service_type` (topup_type from `/topup/operators/{id}/service-types`) after carrier lookup.\n\nLegacy checkout with separate `tool_id` / `denomination_id` fields is still accepted but\ndeprecated; always prefer `price_token` from this response or from `GET /topup/products/{operator_id}`.\n","tags":["Top-up"],"parameters":[{"name":"id","in":"path","required":true,"description":"Operator ID (can be parent or child from service-types)","schema":{"type":"string"},"example":"5123"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"q","in":"query","description":"Keyword search on product name and description (bundle/data features)","schema":{"type":"string"},"example":"5GB whatsapp"},{"name":"service_type","in":"query","description":"Filter by topup_type ID (from `/topup/operators/{id}/service-types`)","schema":{"type":"string"}}],"responses":{"200":{"description":"Denominations and pricing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"id_operator":5123,"operator_name":"Telcel","country_name":"Mexico","iso_short_code":"mx","rates":[{"token":"mexico-telcel-20-mxn-0__2945583__35767699__1.00__EUR","product_slug":"mexico-telcel-20-mxn-0","product_name":"Internet 30 MXN","received_amount":"20","received_currency":"MXN","cost_eur":"1.00","cost_usd":"1.08","topup_type":3,"description":"220 MB de datos, WhatsApp ilimitado","operator":"Telcel Datos Moviles","country":"Mexico","image":"https://www.doctorsim.com/img/oper/5123.gif"}]},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/topup/service-types":{"get":{"operationId":"listTopupServiceTypes","x-vertical":"topup","summary":"List all service type definitions","description":"Returns all available service type categories (airtime, bundles, data, internet, etc.).\nGuest responses are origin-cacheable (`Cache-Control: public, max-age=60, stale-while-revalidate=300`, `Vary: X-App-Platform`, ETag).\n","tags":["Top-up"],"responses":{"200":{"description":"List of service types","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/topup/promotions":{"get":{"operationId":"listTopupPromotions","x-vertical":"topup","summary":"Get active promotions for a country","description":"Returns currently active promotions and special offers for operators in a country.","tags":["Top-up"],"parameters":[{"name":"country","in":"query","required":true,"description":"Country ID","schema":{"type":"string"},"example":"34"}],"responses":{"200":{"description":"Active promotions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/topup/products/search":{"get":{"operationId":"searchTopupProducts","x-vertical":"topup","summary":"Search the top-up catalog (mobile operators)","description":"Same search as the un-prefixed `GET /products/search`, restricted to non-gift-card\ncategories (Mobile, Voice, Utilities). Gift card brands live under `GET /giftcards/brands`.\n\nBrowse the catalog by country, operator, service type, or keyword (operator/brand names,\nservice type labels). Country is required.\n\n**Mobile top-ups with a phone number:** use `GET /topup/carriers/lookup/{phone}` first, then\n`/topup/operators/{id}/service-types` and `/topup/operators/{id}/rates?q=…` — not catalog search alone.\n\n**Rate description search:** when both `operator_id` and `q` are set, returns live rates\nwhose `product_name` or `description` match the query (`meta.result_type` = `rates`).\n\n**Pagination (catalog mode):** `meta.has_more` and `meta.cursor`; default page size 50 (max 100).\n","tags":["Top-up"],"parameters":[{"name":"country","in":"query","required":true,"description":"ISO 3166-1 alpha-2 code (for example \"es\") or a numeric id_pais (1-6 digits). Any other value is rejected with 400.","schema":{"type":"string"},"example":"es"},{"name":"operator_id","in":"query","description":"Numeric operator id (`id_operator`). Non-numeric values are rejected with 400.","schema":{"type":"string"},"example":"13978"},{"name":"service_type","in":"query","description":"Numeric topup_type id (from `/service-types`). Non-numeric values are rejected with 400.","schema":{"type":"string"}},{"name":"q","in":"query","description":"Catalog keyword, matched as a literal substring (max 100 characters). With operator_id, searches bundle/data descriptions in memory.","schema":{"type":"string"}},{"name":"lang","in":"query","description":"Response language (rate description search mode)","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"limit","in":"query","description":"Page size. Digits only; values outside 1-100 are clamped. Non-numeric values are rejected with 400. Default 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Keyset pagination cursor (`meta.cursor` from the previous page). Must be digits. An invalid cursor is rejected with 400.","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated catalog results or rate matches (when operator_id + q)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":[{"id_operator":13978,"operator_name":"Orange Bundle","country_name":"Egypt","iso_short_code":"eg","continent":"AF","topup_type":6,"service_type":"BUNDLE","service_type_category":"Mobile","img":"https://www.doctorsim.com/img/oper/5342.gif","id_parent_operator":11326}],"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z","cursor":"12854","has_more":true}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/topup/products/{operator_id}":{"get":{"operationId":"getTopupProductDetail","x-vertical":"topup","summary":"Get product detail for an operator","description":"Returns full detail for a specific operator including `available_service_types` and a flat\n`rates` array. Each rate includes a `token` for use as `price_token` in `POST /orders`.\nPricing is fetched in real-time from the provider network.\n","tags":["Top-up"],"parameters":[{"name":"operator_id","in":"path","required":true,"description":"Operator ID (`id_operator`)","schema":{"type":"string"},"example":"5123"},{"name":"lang","in":"query","description":"Response language for descriptions","schema":{"type":"string","enum":["en","es"],"default":"en"}}],"responses":{"200":{"description":"Full product detail with pricing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/giftcards/countries":{"get":{"operationId":"listGiftcardCountries","x-vertical":"giftcards","summary":"List gift card countries","description":"Countries with at least one active gift card brand (parent category `Giftcard` / `Shopping`).\n\nReturns all countries where doctorSIM offers mobile top-up or gift card services. `data` is keyed by locale (`es`, `en`, …); each value is an array of country objects with agent-friendly field names.\n\n`img` is an absolute URL on `https://www.doctorsim.com` (SVG country flag under `/img/country_flags/vectored/`). Legacy `img1.doctorsim.com` URLs are not used.\n\nGuest responses are origin-cacheable (`Cache-Control: public, max-age=60, stale-while-revalidate=300`, `Vary: X-App-Platform`, ETag). Authenticated/sandbox stay `private, no-store`.\n","tags":["Gift cards"],"responses":{"200":{"description":"List of countries by locale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"en":[{"id_country":34,"iso_short_code":"es","name_en":"Spain","slug_en":"spain","continent":"EU","img":"https://www.doctorsim.com/img/country_flags/vectored/es.svg"},{"id_country":1144,"iso_short_code":"mx","name_en":"Mexico","slug_en":"mexico","continent":"NA","img":"https://www.doctorsim.com/img/country_flags/vectored/mx.svg"}]},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/giftcards/brands":{"get":{"operationId":"searchGiftcardBrands","x-vertical":"giftcards","summary":"Search gift card brands","description":"Same search as the un-prefixed `GET /products/search`, restricted to gift card brands\n(parent category `Giftcard` / `Shopping`). Each brand is an `id_operator`; load\ndenominations with `GET /giftcards/brands/{id}/products`.\n\nBrowse the catalog by country, operator, service type, or keyword (operator/brand names,\nservice type labels). Country is required.\n\n**Mobile top-ups with a phone number:** use `GET /topup/carriers/lookup/{phone}` first, then\n`/topup/operators/{id}/service-types` and `/topup/operators/{id}/rates?q=…` — not catalog search alone.\n\n**Rate description search:** when both `operator_id` and `q` are set, returns live rates\nwhose `product_name` or `description` match the query (`meta.result_type` = `rates`).\n\n**Pagination (catalog mode):** `meta.has_more` and `meta.cursor`; default page size 50 (max 100).\n","tags":["Gift cards"],"parameters":[{"name":"country","in":"query","required":true,"description":"ISO 3166-1 alpha-2 code (for example \"es\") or a numeric id_pais (1-6 digits). Any other value is rejected with 400.","schema":{"type":"string"},"example":"es"},{"name":"operator_id","in":"query","description":"Numeric operator id (`id_operator`). Non-numeric values are rejected with 400.","schema":{"type":"string"},"example":"13978"},{"name":"service_type","in":"query","description":"Numeric topup_type id (from `/service-types`). Non-numeric values are rejected with 400.","schema":{"type":"string"}},{"name":"q","in":"query","description":"Catalog keyword, matched as a literal substring (max 100 characters). With operator_id, searches bundle/data descriptions in memory.","schema":{"type":"string"}},{"name":"lang","in":"query","description":"Response language (rate description search mode)","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"limit","in":"query","description":"Page size. Digits only; values outside 1-100 are clamped. Non-numeric values are rejected with 400. Default 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Keyset pagination cursor (`meta.cursor` from the previous page). Must be digits. An invalid cursor is rejected with 400.","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated catalog results or rate matches (when operator_id + q)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":[{"id_operator":13978,"operator_name":"Orange Bundle","country_name":"Egypt","iso_short_code":"eg","continent":"AF","topup_type":6,"service_type":"BUNDLE","service_type_category":"Mobile","img":"https://www.doctorsim.com/img/oper/5342.gif","id_parent_operator":11326}],"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z","cursor":"12854","has_more":true}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/giftcards/brands/{id}/products":{"get":{"operationId":"getGiftcardBrandProducts","x-vertical":"giftcards","summary":"Gift card denominations for a brand","description":"Denominations (with `token` → `price_token`, service fee, and total) for a gift card brand.\nSame response shape as `GET /topup/operators/{id}/rates`; `phone` is never required at checkout.\n\nReturns a flat `rates` array with agent-friendly pricing. Each rate includes human-readable\nfields (`product_name`, `description` for bundle/data details) and a stable `token` (for example\n`mexico-telcel-20-mxn-0__2945583__35767699__1.00__EUR`) — pass that value as `price_token`\nin `POST /orders`.\n\nUse `q` to search bundle/data descriptions (e.g. `q=5GB whatsapp`). Combine with\n`service_type` (topup_type from `/topup/operators/{id}/service-types`) after carrier lookup.\n\nLegacy checkout with separate `tool_id` / `denomination_id` fields is still accepted but\ndeprecated; always prefer `price_token` from this response or from `GET /topup/products/{operator_id}`.\n","tags":["Gift cards"],"parameters":[{"name":"id","in":"path","required":true,"description":"Operator ID (can be parent or child from service-types)","schema":{"type":"string"},"example":"5123"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"q","in":"query","description":"Keyword search on product name and description (bundle/data features)","schema":{"type":"string"},"example":"5GB whatsapp"},{"name":"service_type","in":"query","description":"Filter by topup_type ID (from `/topup/operators/{id}/service-types`)","schema":{"type":"string"}}],"responses":{"200":{"description":"Denominations and pricing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"id_operator":5123,"operator_name":"Telcel","country_name":"Mexico","iso_short_code":"mx","rates":[{"token":"mexico-telcel-20-mxn-0__2945583__35767699__1.00__EUR","product_slug":"mexico-telcel-20-mxn-0","product_name":"Internet 30 MXN","received_amount":"20","received_currency":"MXN","cost_eur":"1.00","cost_usd":"1.08","topup_type":3,"description":"220 MB de datos, WhatsApp ilimitado","operator":"Telcel Datos Moviles","country":"Mexico","image":"https://www.doctorsim.com/img/oper/5123.gif"}]},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/giftcards/products/{operator_id}":{"get":{"operationId":"getGiftcardProductDetail","x-vertical":"giftcards","summary":"Gift card brand detail","description":"Brand detail with live denominations. Same response shape as `GET /topup/products/{operator_id}`.\n\nReturns full detail for a specific operator including `available_service_types` and a flat\n`rates` array. Each rate includes a `token` for use as `price_token` in `POST /orders`.\nPricing is fetched in real-time from the provider network.\n","tags":["Gift cards"],"parameters":[{"name":"operator_id","in":"path","required":true,"description":"Operator ID (`id_operator`)","schema":{"type":"string"},"example":"5123"},{"name":"lang","in":"query","description":"Response language for descriptions","schema":{"type":"string","enum":["en","es"],"default":"en"}}],"responses":{"200":{"description":"Full product detail with pricing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/esim/destinations":{"get":{"operationId":"listEsimDestinations","x-vertical":"esim","summary":"List eSIM destinations","description":"Returns countries and regions with published travel eSIM plans, including\nstarting prices and plan counts. `country_iso` is ISO 3166-1 alpha-2\n(`es`, not `ESP`), same as top-up country codes and storefront routes.\nRegional/global destinations use `eu`, `ww`, `as`.\n\nReturns `404 ESIM_DISABLED` when travel eSIM is not offered on the host.\n\nSandbox API keys (`test_*`) list the **sandbox** catalog. Live keys\nlist the **production** catalog.\n\nGuest responses are origin-cacheable (`Cache-Control: public, max-age=60, stale-while-revalidate=300`, `Vary: X-App-Platform`, ETag). Sandbox and authenticated stay `private, no-store`.\n","tags":["Travel eSIM"],"parameters":[{"name":"coverage_type","in":"query","description":"Filter by coverage type (country, region, global)","schema":{"type":"string"}}],"responses":{"200":{"description":"Destination list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/esim/products":{"get":{"operationId":"listEsimProducts","x-vertical":"esim","summary":"List eSIM plans","description":"Browse travel eSIM plans for a destination. Each plan includes `catalog_id`,\nretail price, data allowance, `unlimited` (boolean: true when the plan has\nunlimited data), validity, and a signed `price_token` for checkout.\n\nOptional `data_type` filters the list: omit or `all` returns both unlimited\nand limited plans; `unlimited` or `limited` returns only that kind.\n\nSandbox keys (`test_*`) return the sandbox catalog. Live keys return\nthe production catalog. Do not mix tokens across environments.\n","tags":["Travel eSIM"],"parameters":[{"name":"country_iso","in":"query","description":"ISO 3166-1 alpha-2 from `/esim/destinations` (`es`, `us`).\nRegional/global destinations use `eu`, `ww`, `as`.\nLegacy ISO-3 (`ESP`) is accepted and translated internally.\n","schema":{"type":"string"},"example":"es"},{"name":"coverage_type","in":"query","schema":{"type":"string"}},{"name":"data_type","in":"query","description":"Optional filter by data allowance. Default `all` (no filter).","schema":{"type":"string","enum":["all","unlimited","limited"],"default":"all"}},{"name":"lang","in":"query","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"currency","in":"query","schema":{"type":"string","enum":["EUR","USD"],"default":"EUR"}}],"responses":{"200":{"description":"Plan list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/esim/products/{catalog_id}":{"get":{"operationId":"getEsimProductDetail","x-vertical":"esim","summary":"Get eSIM plan detail","description":"Full detail for a single eSIM catalog SKU, including `unlimited` (boolean)\nand `data_gb`.\n","tags":["Travel eSIM"],"parameters":[{"name":"catalog_id","in":"path","required":true,"schema":{"type":"string"},"example":"42"},{"name":"lang","in":"query","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"currency","in":"query","schema":{"type":"string","enum":["EUR","USD"],"default":"EUR"}}],"responses":{"200":{"description":"Plan detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/esim/orders/preview":{"post":{"operationId":"previewEsimOrder","x-vertical":"esim","x-alias-of":"previewOrder","summary":"Preview eSIM checkout (alias of POST /orders/preview)","description":"Vertical-specific alias of `POST /orders/preview` — the shared endpoint\naccepts the same body and infers `type: esim`. Kept so existing\nintegrations keep working; new integrations should use `/orders/preview`.\n\nSame body as `POST /esim/orders`. Returns the price breakdown, `unlimited`\n(boolean), `data_gb`, and `checkout_mode` **without** creating the order\nor deducting credits.\n\nPRO accounts see `checkout_mode=credits`; guest / consumer OAuth see\n`payment_link`. Requires `orders:read` when authenticated.\n\nSandbox keys preview the sandbox catalog. Live keys preview production.\n","tags":["Travel eSIM"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EsimCreateOrderRequest"}}}},"responses":{"200":{"description":"Price preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Account is not eligible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid price_token or SKU","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/esim/orders":{"post":{"operationId":"createEsimOrder","x-vertical":"esim","x-alias-of":"createOrder","summary":"Create a travel eSIM order (alias of POST /orders)","description":"Vertical-specific alias of `POST /orders` — the shared endpoint accepts\nthe same body and infers `type: esim`. Kept so existing integrations\nkeep working; new integrations should use `/orders`.\n\nPlaces a travel eSIM order after preview. Same body as preview.\n\n**Checkout modes** (same as top-up / gift cards):\n- **PRO account**: debits prepaid credits and returns `order_id`.\n- **Consumer OAuth / guest**: returns `payment_link` (show this), creator `email`, and `order_id` when a ticket exists. `checkout_hash` is machine-only. Same `email` + `order_id` pair as `POST /orders/guest-lookup`.\n\nRequires a `read-write` API key or OAuth `orders:write`.\n\n**Sandbox keys (`test_*`):** create a fake eSIM from the sandbox\ncatalog (no production credits, no ticket). Returns\n`sandbox_esim_{id}` immediately with a synthetic ICCID / LPA.\nPoll `GET /orders/{id}` or `GET /orders?type=esim`.\n\n**Live keys:** production catalog + production checkout (real credit debit).\n\nConfirmation is emailed to the titular account.\n","tags":["Travel eSIM"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EsimCreateOrderRequest"}}}},"responses":{"201":{"description":"Order created (`order_id` on credit checkout) or payment link issued (`checkout_mode=payment_link`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Insufficient PRO credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Account is not eligible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Order creation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/esim/orders/{id}/activate":{"post":{"operationId":"activateEsimOrder","x-vertical":"esim","summary":"Activate a paid eSIM (Bearer owner, native preferred)","description":"Owner claim for a paid eSIM waiting on Activate (`esim_state=awaiting_activation`).\nWraps the same web claim (IP / fraud recording, idempotent if already\nCOMPLETED) and returns the `GET /orders/{id}` payload, including install\nfields once provisioned.\n\nHTTP stays **200**. Additive `activation` object:\n`activation.result` = `ready` | `provisioning` | `failed`,\n`activation.error_code` string or null. A still-pending wholesale\npurchase is `provisioning` (not a 5xx). Poll `GET /orders/{id}` until\n`esim_state=ready`. In-request provider wait is capped at ~20s.\n\n`lang` is forced to `es` or `en` (default `en`). Optional\n`source=ios_app|android_app`. When `lpa_string` parses as\n`LPA:1$smdp$matching_id`, `smdp_address` and `matching_id` are included.\n\nFraud / manual review is `esim_state=under_review` (never\n`activate_esim`; POST returns 403 `CLAIM_BLOCKED`). Rate-limited\n(5/min/IP, 10/hour/ticket). Requires `orders:write` and a linked Bearer.\nNo MCP tool — native / first-party only.\n","tags":["Travel eSIM"],"parameters":[{"name":"id","in":"path","required":true,"description":"User-visible ticket / order id","schema":{"type":"string"},"example":"8299680"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"lang":{"type":"string","enum":["en","es"],"default":"en"},"source":{"type":"string","enum":["ios_app","android_app","account"]}}},"example":{"lang":"es","source":"ios_app"}}}},"responses":{"200":{"description":"Order-detail payload after claim","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Claim blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/esim/lines/{iccid}":{"get":{"operationId":"getEsimLine","x-vertical":"esim","summary":"Get remaining data for a titular-owned eSIM","description":"Returns remaining data / balance for an eSIM line owned by the authenticated\ntitular. ICCID comes from a fulfilled `GET /orders/{id}` (`iccid` field).\nThe line is ICCID-scoped (same remaining data on every device).\n\nRequires `orders:read` and a linked Bearer. No `Authorization`\nheader returns `401 MISSING_AUTH`. A non-Bearer scheme\n(`Basic`, …) returns `401 INVALID_AUTH_SCHEME`. Guest checkout\ncannot call this endpoint. A missing ICCID or a line the caller\ndoes not own returns `404 LINE_NOT_FOUND` (\"eSIM line not found\nfor this account.\") — the same copy as E1 / list / detail / hash\nownership misses. It is not `ORDER_FAILED`.\n\nThe handler refreshes from the provider when `balance_checked_at` is\nmissing or older than five minutes. `refresh=1` is accepted for\ncompatibility and follows the same five-minute gate.\n","tags":["Travel eSIM"],"parameters":[{"name":"iccid","in":"path","required":true,"description":"ICCID digits (10–22) from a fulfilled eSIM order","schema":{"type":"string","pattern":"^[0-9]{10,22}$"},"example":"8944123456789012345"},{"name":"refresh","in":"query","description":"Accepted for compatibility. Provider refresh still runs only when the last check is older than five minutes.","schema":{"type":"string","enum":["0","1","true"]}}],"responses":{"200":{"description":"Line detail with remaining data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/esim/lines/{iccid}/topup-plans":{"get":{"operationId":"getEsimLineTopupPlans","x-vertical":"esim","summary":"List top-up packs for an owned eSIM","description":"Exact storefront / Mi Cuenta tiers for this ICCID\n(`EsimTopupCatalog::getPlanPayloadsForParent`). No custom amount.\nEach plan includes a signed `price_token` on catalog `retail_eur`\n(the same amount PRO credits debit). PRO accounts receive\n`checkout_mode=credits`. Requires `orders:read` and a linked\nBearer. No `Authorization` header returns `401 MISSING_AUTH`.\nA non-Bearer scheme (`Basic`, …) returns `401 INVALID_AUTH_SCHEME`.\n","tags":["Travel eSIM"],"parameters":[{"name":"iccid","in":"path","required":true,"description":"ICCID digits of the original (non top-up) eSIM line","schema":{"type":"string","pattern":"^[0-9]{10,22}$"},"example":"8944123456789012345"},{"name":"lang","in":"query","description":"Plan titles language","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"currency","in":"query","description":"Display currency for price_token","schema":{"type":"string","enum":["EUR","USD"],"default":"EUR"}}],"responses":{"200":{"description":"Parent summary plus compatible top-up plans","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/esim/lines/{iccid}/topups/preview":{"post":{"operationId":"previewEsimLineTopup","x-vertical":"esim","summary":"Preview an eSIM top-up (no charge)","description":"Validates parent ownership, SKU compatibility, and `price_token`.\nReturns the same pricing shape as eSIM preview plus `is_topup` and\n`iccid`. When `currency=USD`, pricing includes `retail_usd` /\n`amount_due_usd`. PRO accounts see `checkout_mode=credits` and credit\nredemption when the wallet covers catalog `retail_eur`.\nRequires a linked account (no guest) and `orders:read`.\nNo `Authorization` header returns `401 MISSING_AUTH`.\nA non-Bearer scheme (`Basic`, …) returns `401 INVALID_AUTH_SCHEME`.\n","tags":["Travel eSIM"],"parameters":[{"name":"iccid","in":"path","required":true,"description":"ICCID of the original eSIM line","schema":{"type":"string","pattern":"^[0-9]{10,22}$"},"example":"8944123456789012345"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["catalog_id","price_token"],"properties":{"catalog_id":{"type":"integer"},"price_token":{"type":"string"},"lang":{"type":"string","enum":["en","es"]},"currency":{"type":"string","enum":["EUR","USD"]}}},"example":{"catalog_id":1291,"price_token":"esim__example","lang":"en","currency":"EUR"}}}},"responses":{"200":{"description":"Top-up preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/esim/lines/{iccid}/topups":{"post":{"operationId":"createEsimLineTopup","x-vertical":"esim","summary":"Create an eSIM top-up checkout","description":"Consumer OAuth: hosted `payment_link` whose hash row uses\n`ESM{catalogId}T{parentId}`. Send `Idempotency-Key` (replay returns\nthe same order). PRO: debit catalog `retail_eur` from\ncredits (same path as a new eSIM) and return `order_id` +\n`credits_used`. Insufficient wallet is `402 INSUFFICIENT_CREDITS`\nwith no debit. `Idempotency-Key` replay does not double-debit.\nFulfilment stays `is_topup=1` (no new QR). Native sandbox accounts\nget a sandbox top-up row that fulfills after about 30 seconds.\nRequires a linked account (no guest) and `orders:write`.\nNo `Authorization` header returns `401 MISSING_AUTH`.\nA non-Bearer scheme (`Basic`, …) returns `401 INVALID_AUTH_SCHEME`.\n","tags":["Travel eSIM"],"parameters":[{"name":"iccid","in":"path","required":true,"description":"ICCID of the original eSIM line","schema":{"type":"string","pattern":"^[0-9]{10,22}$"},"example":"8944123456789012345"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["catalog_id","price_token"],"properties":{"catalog_id":{"type":"integer"},"price_token":{"type":"string"},"lang":{"type":"string","enum":["en","es"]},"currency":{"type":"string","enum":["EUR","USD"]}}},"example":{"catalog_id":1291,"price_token":"esim__example","lang":"en","currency":"EUR"}}}},"responses":{"201":{"description":"Hosted payment link, PRO credits order, or sandbox top-up row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Insufficient PRO credits (`INSUFFICIENT_CREDITS`) — no debit"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}},"/esim/qr/{ticket_id}/{sig}":{"get":{"operationId":"getEsimQrImage","x-vertical":"esim","summary":"Hosted eSIM activation QR as PNG","description":"Returns a PNG QR of the stored LPA for a fulfilled eSIM order.\nAuth is a 16-character HMAC in the path so `<img src>` works without\nan API key. Unknown or invalid signatures return 404 text `Not found`.\n\n**Ops (roadmap):** add a Cloudflare WAF rate-limit on `/v2/esim/qr/*`\nbefore production. The HMAC is not enough against brute force.\n","tags":["Travel eSIM"],"security":[],"parameters":[{"name":"ticket_id","in":"path","required":true,"description":"Zendesk ticket id (same as `order_id` on list/detail)","schema":{"type":"string"},"example":"8106157"},{"name":"sig","in":"path","required":true,"description":"16-character HMAC from `qr_code_url`","schema":{"type":"string","pattern":"^[0-9a-f]{16}$"}}],"responses":{"200":{"description":"PNG image (`Content-Type: image/png`)","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Not found"}}}},"/orders/preview":{"post":{"operationId":"previewOrder","x-vertical":"core","summary":"Preview the exact price breakdown for an order (no order placed)","description":"One preview endpoint for every vertical. Returns the authoritative,\naccount-specific price breakdown for a prospective order — cost,\nservice fee, SMS fee (top-up, if requested), total, and credit applied —\n**without creating the order or deducting credits**.\n\nAccepts the same body as `POST /orders`. The vertical is inferred from\nthe body: `type: esim`, an eSIM `price_token`, or `catalog_id` selects\nthe travel eSIM checkout; everything else is a top-up / gift card preview.\nPRO accounts see `checkout_mode=credits`; guest / consumer OAuth see\n`payment_link`. A guest body whose `email` belongs to a PRO account\nreturns `403 ACCOUNT_IS_PRO` (sign in; guest checkout is not allowed).\nRequires a `read` scoped API key or OAuth token\n(`orders:read`). Optional native campaign fields (`utm_*`, click ids,\n`cio_link_id`) are accepted on the same body as create; preview does\nnot persist them.\n\nFees mirror the website checkout exactly, including PRO/VIP tier\noverrides and country/operator exceptions. The SMS fee (0.99 EUR) is\nonly included when `sms_notification=true`. The preview response does\nnot include the SMS body. When `sms_text` is omitted, a default\nlocalized message is used later, on the order itself.\n","tags":["Core — Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"},"examples":{"priceTokenTopup":{"summary":"Preview a top-up with price_token","value":{"price_token":"mexico-telcel-20-mxn-3__2945583__35767699__1.00__EUR","phone":"+522212345678","lang":"en","sms_notification":true}},"esimPreview":{"summary":"Preview a travel eSIM (PRO credits)","value":{"type":"esim","catalog_id":2353,"price_token":"esim__2353__2.99__EUR__0__1710000000__a1b2c3d4e5f6g7h8","lang":"en"}}}}}},"responses":{"200":{"description":"Price breakdown","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"environment":"production","currency_basis":"EUR","pricing":{"cost_eur":"20.00","cost_usd":"21.80","service_fee_eur":"0.99","service_fee_usd":"0.99","sms_notification":true,"sms_fee_eur":"0.99","sms_fee_usd":"0.99","total_cost_eur":"21.98","total_cost_usd":"23.78","credit_redeemed_eur":"0.00","amount_due_eur":"21.98"}},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Guest email belongs to a PRO account (`ACCOUNT_IS_PRO`) — sign in required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders":{"post":{"operationId":"createOrder","x-vertical":"core","summary":"Create an order (top-up, gift card, or travel eSIM)","description":"Places an order after `POST /orders/preview`. Requires `orders:write` scope.\nOne endpoint for every vertical: the body decides. `type: esim`, an eSIM\n`price_token`, or `catalog_id` routes to the travel eSIM checkout\n(sandbox keys create a fake eSIM order on credit checkout). Any other\nbody is a top-up / gift card order. The response always carries `type`.\n\nSuccessful PRO credit checkout always returns `order_id`, `status`, and\n`credits_used`. Never treat payment-gateway HTML or receipt text as the\nsuccess body. If a response lacks `order_id`, call `GET /orders` or\n`GET /orders/{id}` — do not create again.\n\nSend `Idempotency-Key` (header) and/or `idempotency_key` (JSON). Same key\n+ same credential + same body replays the original response for 24 hours\n(`Idempotent-Replayed: true`, no second order, no second charge). Multi-SKU:\none create per denomination with a **distinct** key. Same key + different\nbody → `409 IDEMPOTENCY_CONFLICT`. A retry while the first request with that\nkey is still running → `409 IDEMPOTENCY_IN_PROGRESS` (`Retry-After`); retry\nthe same request. If the first request never recorded an outcome →\n`409 IDEMPOTENCY_OUTCOME_UNKNOWN` (check `GET /orders` before using a new key).\n\n**Create cap:** 10 order creates per 10 minutes per principal. An\nauthenticated caller is one bucket per credential (the same principal as\nIdempotency-Key). A guest is one bucket per client IP. The 11th create\nin the window is `429` `RATE_LIMITED` with `Retry-After` and does not\ncreate an order. A replay of a stored Idempotency-Key does not count.\nThis cap is separate from the per-minute credential tier limit.\n\n**Recovering after a timeout:** if the connection dropped before you got the\n`order_id`, call `GET /orders/lookup?idempotency_key=<same key>` (read-only,\nany credential of the same account and environment). `404 ORDER_NOT_FOUND`\nthere means no order exists for that key, so re-sending `POST /orders` with the\nsame key and body is safe.\n\n**`credits_used` (PRO credits):** the gross amount debited from the prepaid\nbalance in EUR — product cost **plus** the service fee **plus** the SMS fee\nwhen `sms_notification=true`. It equals the gross total shown by\n`POST /orders/preview`. It is set at create time and never rewritten: a later\nrefund appears as a positive line in `GET /balance/history` and as\n`status: refunded` on `GET /orders/{id}`. The amount the recipient receives\n(face value, e.g. 20 MXN) is `pricing.received_amount` /\n`pricing.received_currency` in the preview response (also `received_amount` on\neach rate) when the product has a fixed face value.\n\n**Checkout modes:**\n- **PRO account**: debits prepaid credits; returns `order_id`, `status`, `credits_used`.\n- **Consumer OAuth / guest**: returns `payment_link` (show this), creator `email`, and `order_id`\n  when a ticket exists. Store those two fields for `POST /orders/guest-lookup` or hash status.\n  User pays on doctorsim.com and receives the product by email. `checkout_hash` is machine-only\n  (poll via `payment_link` or `checkout_hash`; that poll also returns `email` + ticket `order_id`).\n\n**Recommended flow (price_token):**\n1. `GET /topup/carriers/lookup/{phone}` or browse `/products/search` to pick an operator\n2. `GET /topup/operators/{id}/rates` (or `GET /topup/products/{operator_id}`) and copy a rate `token`\n3. `POST /orders` with `price_token` (+ `phone` for top-ups; gift cards may omit phone)\n\nWith `price_token`, only `phone` is required for mobile top-ups — country and operator are\nresolved from the token. Optional `operator_id` / `country_id` act as cross-checks.\n\n**Legacy:** Sending `tool_id`, `denomination_id`, `amount`, `operator_id`, `country_id`, and\n`currency` without `price_token` is still supported but deprecated.\n\n**Sandbox** (`test_*` keys / OAuth `env: sandbox`): no credits are debited and\nnothing is delivered. The response carries an opaque `order_id` such as\n`sandbox_01ca75fd73c6202d`, `credits_used: \"0.00\"`, `simulated_credits_used`\n(what a live key would have debited) and `sandbox_outcome`. Poll\n`GET /orders/{order_id}` with the same account: `processing` for ~3 seconds,\nthen the simulated terminal state (`fulfilled` by default; choose `failed`,\n`cancelled` or `refunded` with `sandbox_outcome`, or a phone ending in\n`0001` / `0002` / `0003`). Registered webhooks receive `order.created`, then\n`order.fulfilled` or `order.failed`. Sandbox orders also appear in\n`GET /orders` and `GET /balance/history` for sandbox credentials.\n\n**Production** (`live_*` keys / OAuth `env: production`): credits deducted; real fulfillment.\n\n**Native app (Rork iOS / Android):** send `X-App-Platform`, `X-App-Version`,\n`X-App-Build`, and `X-App-Client` (or body `app_platform` / `app_version` /\n`app_build` / `app_client` / `client_source`). Optional campaign fields on\nthe JSON body (`utm_*`, `gclid`, `gbraid`, `wbraid`, `cio_link_id`, and\nother click ids) are persisted on the order and the guest `checkout_hash`\nstore, then copied onto `payment_link` so the hosted checkout in\nSFSafariView keeps campaign context without www cookies. Preview accepts\nthe same fields (no persist). `asa_attribution_token` is stored only;\nApple Ads client fetch is not part of this contract. Do not send ATT/IDFA.\nSee schema `NativeCampaignAttribution`.\n","tags":["Core — Orders"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"1–128 printable ASCII. Same key + same account + same body returns\nthe stored 2xx body (`Idempotent-Replayed: true`). Header wins over\nJSON `idempotency_key`. Use a new key for each distinct SKU.\n","schema":{"type":"string","minLength":1,"maxLength":128}},{"name":"X-App-Platform","in":"header","required":false,"description":"Native OS. `ios` / `iphone` / `ipad` or `android`.","schema":{"type":"string","enum":["ios","iphone","ipad","ipados","android"]}},{"name":"X-App-Version","in":"header","required":false,"description":"App marketing version (max 20).","schema":{"type":"string","maxLength":20}},{"name":"X-App-Build","in":"header","required":false,"description":"App build number (max 40).","schema":{"type":"string","maxLength":40}},{"name":"X-App-Client","in":"header","required":false,"description":"Native client family. `rork` maps to the doctorSIM iOS app.","schema":{"type":"string","enum":["rork","rork_max","rork_ios","rork_android"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"},"examples":{"priceTokenTopup":{"summary":"Top-up with price_token (recommended)","value":{"price_token":"mexico-telcel-20-mxn-3__2945583__35767699__1.00__EUR","phone":"+522212345678","lang":"en","sms_notification":false}},"rorkNativeCampaign":{"summary":"Native iOS app order with campaign fields","value":{"price_token":"mexico-telcel-20-mxn-3__2945583__35767699__1.00__EUR","phone":"+522212345678","lang":"en","client_source":"ios_app","app_client":"rork","utm_source":"google","utm_medium":"cpc","utm_campaign":"rork_ios_spring","utm_content":"banner_a","utm_term":"mexico telcel","gclid":"CjwKCAjwzNTUBhAjEiwA7zcvWsGuRDc0z5hbWZwy7nWC5Lnw5uIeYbkxy8r1QeNJTjTB_2eCf_6pTxoC1CIQAvD_BwE","gbraid":"gbraid-ios-click-id-value-xxxx","cio_link_id":"eyJlIjo1NzIzfQ"}},"priceTokenGiftCard":{"summary":"Gift card with price_token","value":{"price_token":"spain-netflix-25-eur-0__2945585__12806338__25.00__EUR","lang":"en"}},"esimOrder":{"summary":"Travel eSIM with catalog_id + price_token (PRO credits)","value":{"type":"esim","catalog_id":2353,"price_token":"esim__2353__2.99__EUR__0__1710000000__a1b2c3d4e5f6g7h8","lang":"en"}},"legacyCheckout":{"summary":"Legacy tool_id checkout (deprecated)","value":{"operator_id":"5123","country_id":"1144","tool_id":"2945585","denomination_id":"12806338","amount":"50","phone":"+521234567890","currency":"EUR","lang":"en","sms_notification":false}}}}}},"responses":{"201":{"description":"Order created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"examples":{"creditOrder":{"summary":"Production PRO credit checkout (live_ key / OAuth env production)","value":{"success":true,"data":{"order_id":"8304512","status":"placed","checkout_mode":"credits","code":"ORDER_CREATED","message":"Order created successfully.","credits_used":"1.97","currency":"EUR","type":"topup","environment":"production"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:30:12Z"}}},"sandboxCreditOrder":{"summary":"Sandbox PRO credit checkout (test_ key / OAuth env sandbox)","value":{"success":true,"data":{"order_id":"sandbox_01ca75fd73c6202d","status":"processing","checkout_mode":"credits","code":"ORDER_CREATED","message":"Order created successfully.","credits_used":"0.00","currency":"EUR","type":"topup","environment":"sandbox","simulated_credits_used":"1.97","sandbox_outcome":"fulfilled","idempotency_key":"roamify-7f3c2a10-0001"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:30:12Z"}}},"paymentLinkOrder":{"summary":"Consumer OAuth payment link","value":{"success":true,"data":{"checkout_mode":"payment_link","checkout_hash":"a1b2c3...","payment_link":"https://www.doctorsim.com/en/mx/topup-phone/checkout/a1b2c3...","status":"awaiting_payment","email":"buyer@example.com","order_id":null,"expires_at":"2026-06-12T15:00:00Z"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Insufficient credits (PRO accounts only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"INSUFFICIENT_CREDITS","message":"Insufficient credit balance. Current balance: 0.50 EUR. Order requires: 1.66 EUR.","current_balance":"0.50","required_amount":"1.66","currency":"EUR"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"403":{"description":"Write scope required, or guest email belongs to a PRO account (`ACCOUNT_IS_PRO`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`IDEMPOTENCY_CONFLICT` (key reused with a different body),\n`IDEMPOTENCY_IN_PROGRESS` (first request with this key still running — retry the same request),\nor `IDEMPOTENCY_OUTCOME_UNKNOWN` (first request ended without an outcome — check `GET /orders`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"IDEMPOTENCY_IN_PROGRESS","message":"An order with this Idempotency-Key is still being created. Retry the same request in a few seconds, or call GET /v2/orders/lookup?idempotency_key=… — do not use a new key."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:30:13Z"}}}}},"422":{"description":"Order creation failed or gateway receipt without order_id (ORDER_STATUS_UNKNOWN). `ORDER_STATUS_UNKNOWN` is kept for the Idempotency-Key, so a retry with the same key replays it instead of creating again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"operationId":"listOrders","x-vertical":"core","summary":"List orders","description":"Returns a paginated list of orders for the authenticated account (completed and\nrefunded), enriched with operator/country metadata. Uses keyset pagination via\nthe `cursor` parameter.\n\n**Sandbox credentials** list their own `sandbox_*` orders (every simulated\nstate, newest first); `cursor` is the last `order_id` of the previous page.\nProduction credentials never see sandbox orders and vice versa.\n\nIncludes orders with `payment_status` **Completed** or **Refunded**. The `status`\nfield is `refunded` when the payment was refunded; `delivery_status` reflects the\ntopup delivery pipeline status. Pass `type=esim` for travel eSIM history (also\ndocumented under the eSIM tag). Fulfilled eSIM rows include `iccid`,\n`install_url`, and hosted `qr_code_url`. `balance_amount` is\n`per order basis` on the list (no provider call). Native My eSIMs\nshould decode nested `consumption` (cron snapshot:\n`activated`, `unlimited`, `data_remaining_gb`, `days_left`).\nDefault `type` is top-up.\n","tags":["Core — Orders"],"parameters":[{"name":"type","in":"query","description":"Filter by order type (default topup)","schema":{"type":"string","enum":["topup","giftcard","esim"],"default":"topup"}},{"name":"limit","in":"query","description":"Max results per page (1-100, default 25)","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","description":"Keyset pagination cursor (`order_id` / `ticket_id` from previous page's meta.cursor). Results are ordered by `ticket_id` DESC.","schema":{"type":"string"}},{"name":"delivery_status","in":"query","description":"Filter by delivery status","schema":{"type":"string","enum":["INIT","SUBMITTED","PROCESSING","COMPLETE","COMPLETED","FAILED","CANCELLED","RESERVED"]}}],"responses":{"200":{"description":"Paginated list of completed orders (newest `order_id` first)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":[{"order_id":7337221,"type":"topup","status":"fulfilled","delivery_status":"COMPLETED","amount":"6.29","currency":"EUR","phone":"34666123123","created_at":"2026-03-19 09:32:48","id_operator":5123,"operator_name":"Orange","country_name":"Spain","iso_short_code":"es","service_type":"AIRTIME","service_type_category":"Mobile","img":"https://www.doctorsim.com/img/oper/5342.gif"}],"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z","cursor":"6340356","has_more":true}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/orders/{id}":{"get":{"operationId":"getOrderStatus","x-vertical":"core","summary":"Get order status","description":"Check the current status of an order. Use the exact `order_id` returned by\n`POST /orders`: a numeric ticket in production, an opaque `sandbox_…` id in\nsandbox (sandbox credentials of the same account only). Possible statuses:\n- `processing` - Order placed, being fulfilled (`delivery_status` PROCESSING / SUBMITTED / INIT)\n- `fulfilled` - Top-up/gift card delivered successfully (`delivery_status: COMPLETED`)\n- `failed` - Delivery failed (`delivery_status: FAILED`, see `error_code` / `error_description`)\n- `cancelled` - Order was cancelled before delivery (`delivery_status: CANCELLED`)\n- `refunded` - The payment was refunded (`payment_status: Refunded`); for PRO credit orders the\n  amount is returned to the credit balance as a separate line in `GET /balance/history`\n\n`fulfilled`, `failed`, `cancelled` and `refunded` are terminal for polling;\n`failed` can later become `refunded`. `credits_used` (production PRO credit\norders) is the gross EUR debit including service fee and SMS fee and keeps\nits original value on failed / cancelled / refunded orders; it is `null`\nfor orders not paid with PRO credits. `amount` / `currency` is the amount\ncharged (PRO: same as `credits_used`).\n\n**Money fields (all EUR, gross):**\n\n| Field | Production | Sandbox |\n|---|---|---|\n| `credits_used` | Debit from the PRO balance = preview `total_cost_eur` (cost + service fee + SMS fee); `credit_redeemed_eur` when > 0 | always `\"0.00\"` (nothing is debited) |\n| `simulated_credits_used` | — | What a live key would debit = sandbox preview `total_cost_eur` (cost + service fee + SMS fee) |\n| `amount` | Amount charged (PRO credit orders: equals `credits_used`) | Equals `simulated_credits_used` |\n| `GET /balance/history` | Negative debit line per order (gross) | `amount: \"0.00\"`, `simulated_amount` = `-simulated_credits_used` |\n\nNever the bare product cost (`cost_eur`). Production PRO accounts may have a\ndifferent service fee than the standard fee used by the sandbox preview; the\nproduction preview for your account is authoritative.\n\n**Sandbox:** `processing` for ~3 s after create, then the simulated outcome\nchosen at create (`sandbox_outcome`, default `fulfilled`). `credits_used` is\nalways `0.00`; `simulated_credits_used` is the gross amount a live key would\ndebit (same as the sandbox preview `total_cost_eur`).\nA sandbox credential asking for a numeric (production) id gets\n`403 SANDBOX_READ_DENIED`. Unknown ids and malformed paths return the\nstandard error envelope (`404 ORDER_NOT_FOUND` / `404 NOT_FOUND`).\n\n**Gift cards:** when `type` is `giftcard` and `status` is `fulfilled`, the\nresponse includes `redemption_code` (PIN) or `redemption_url` (clickable\nredeem link), plus optional `redemption_instructions` — same data as the\nwebsite order-status (OSL) flow. Redemption fields are omitted while the\norder is still processing.\n\nGift-card (and list) payloads also include:\n- `created_at` ISO8601 from `paypal_ipn_orders.creation_timestamp`\n- `redeemed_at` ISO8601 from `topup_transactions.solved_timestamp` when\n  fulfilled; `null` if that timestamp is missing (never invented)\n- `giftcard_usage` `{status, amount_used_approx, currency, updated_at}`\n  from aux table `giftcard_usage` (defaults to `unused` when no row)\n\nMark usage with `POST /orders/{id}/giftcard-usage`.\n\n**eSIM:** when `type` is `esim` and `status` is `fulfilled`, the response\nincludes `iccid`, `lpa_string`, `install_url` (Apple), optional\n`smdp_address` + `matching_id` when `lpa_string` is `LPA:1$smdp$matching_id`,\nand a hosted PNG\n`qr_code_url` (`/v2/esim/qr/{ticket_id}/{sig}`), remaining\n`balance_amount` (provider refresh if last check is older than five\nminutes), and nested `consumption` (cron snapshot for days left /\nremaining data / unlimited). `GET /orders?type=esim` lists the same\nfulfillment fields and the same `consumption` object but sets\n`balance_amount` to `per order basis` and does not call the provider.\nUse `GET /esim/lines/{iccid}` for the ICCID-level line\n(device-independent). Guest payment-link checkouts can be polled via\n`GET /orders?payment_link=` or `GET /orders?checkout_hash=`. That poll\nreturns creator `email` and ticket `order_id` when known (same pair as\n`POST /orders/guest-lookup`). Do not show the hash.\n","tags":["Core — Orders"],"parameters":[{"name":"id","in":"path","required":true,"description":"Order ID exactly as returned by POST /orders (numeric ticket in production, `sandbox_…` in sandbox)","schema":{"type":"string"},"example":"sandbox_01ca75fd73c6202d"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"payment_link","in":"query","description":"Guest checkout URL from create order. Preferred status key — do not show the embedded hash.","schema":{"type":"string"}},{"name":"checkout_hash","in":"query","description":"Machine checkout identifier. Prefer payment_link. Never display to the buyer.","schema":{"type":"string"}}],"responses":{"200":{"description":"Order details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"examples":{"productionFulfilled":{"summary":"Production PRO top-up, fulfilled","value":{"success":true,"data":{"order_id":8304512,"type":"topup","status":"fulfilled","delivery_status":"COMPLETED","payment_status":"Completed","amount":"1.97","currency":"EUR","credits_used":"1.97","phone":"5215512345678","created_at":"2026-10-09T09:30:12Z","updated_at":"2026-10-09T09:30:40Z","id_operator":5123,"operator_name":"Telcel","country_name":"Mexico","iso_short_code":"mx","error_code":null,"error_description":null,"environment":"production"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:31:00Z"}}},"productionRefunded":{"summary":"Production PRO top-up, failed delivery then refunded","value":{"success":true,"data":{"order_id":8304513,"type":"topup","status":"refunded","delivery_status":"FAILED","payment_status":"Refunded","amount":"1.97","currency":"EUR","credits_used":"1.97","environment":"production"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T10:05:00Z"}}},"sandboxProcessing":{"summary":"Sandbox, first poll (< 3 s after create)","value":{"success":true,"data":{"order_id":"sandbox_01ca75fd73c6202d","type":"topup","status":"processing","delivery_status":"PROCESSING","payment_status":"Completed","id_operator":5123,"id_country":1144,"phone":"+5215512345678","amount":"1.97","currency":"EUR","credits_used":"0.00","simulated_credits_used":"1.97","error_code":null,"error_description":null,"environment":"sandbox","created_at":"2026-10-09T09:30:12Z","updated_at":"2026-10-09T09:30:13Z"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:30:13Z"}}},"sandboxFulfilled":{"summary":"Sandbox, simulated delivery (default outcome)","value":{"success":true,"data":{"order_id":"sandbox_01ca75fd73c6202d","type":"topup","status":"fulfilled","delivery_status":"COMPLETED","payment_status":"Completed","credits_used":"0.00","simulated_credits_used":"1.97","environment":"sandbox"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:30:20Z"}}},"sandboxFailed":{"summary":"Sandbox, simulated failure (sandbox_outcome=failed or phone ending 0001)","value":{"success":true,"data":{"order_id":"sandbox_9a1e0c55b2d47f10","type":"topup","status":"failed","delivery_status":"FAILED","payment_status":"Completed","credits_used":"0.00","simulated_credits_used":"1.97","error_code":"SANDBOX_SIMULATED_FAILURE","error_description":"Simulated delivery failure (sandbox). No real top-up was attempted.","environment":"sandbox"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:31:20Z"}}}}}}},"403":{"description":"`SANDBOX_READ_DENIED` (sandbox credential + production order id) or\n`INSUFFICIENT_SCOPE` (OAuth token without `orders:read`)\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"`ORDER_NOT_FOUND` (no such order for this account and environment) or\n`NOT_FOUND` (malformed id / unknown route). Always the standard error envelope.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"ORDER_NOT_FOUND","message":"Sandbox order not found: sandbox_0000000000000000"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:30:13Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/orders/lookup":{"get":{"operationId":"lookupOrder","x-vertical":"core","summary":"Find the order created with an Idempotency-Key (read-only recovery)","description":"Recover an order after `POST /orders` timed out before you received the\n`order_id`. Pass the same value you sent as `Idempotency-Key` (or\n`idempotency_key`). Returns the **current** status payload of that order\n(same fields as `GET /orders/{id}`) plus `idempotency_key`. Requires\n`orders:read` (OAuth).\n\n- Works with any credential of the same account and environment for 24 hours\n  (e.g. order created with a sandbox API key, looked up with a sandbox OAuth\n  token).\n- `409 IDEMPOTENCY_IN_PROGRESS`: the first create with this key is still\n  running — retry in a few seconds.\n- `404 ORDER_NOT_FOUND`: no order exists for this key — re-sending\n  `POST /orders` with the same key and body is safe.\n- Never creates or modifies anything.\n","tags":["Core — Orders"],"parameters":[{"name":"idempotency_key","in":"query","required":true,"description":"Same value sent as Idempotency-Key on POST /orders","schema":{"type":"string","minLength":1,"maxLength":128},"example":"roamify-7f3c2a10-0001"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}}],"responses":{"200":{"description":"The order created with that key (current status)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"order_id":"sandbox_01ca75fd73c6202d","type":"topup","status":"fulfilled","delivery_status":"COMPLETED","payment_status":"Completed","credits_used":"0.00","simulated_credits_used":"1.97","environment":"sandbox","idempotency_key":"roamify-7f3c2a10-0001"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T09:31:00Z"}}}}},"400":{"description":"Missing or invalid `idempotency_key` (`INVALID_IDEMPOTENCY_KEY`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No order created with this key for this account in the last 24 hours (`ORDER_NOT_FOUND`) — safe to retry the create with the same key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"First create with this key still running (`IDEMPOTENCY_IN_PROGRESS`) or ended without an outcome (`IDEMPOTENCY_OUTCOME_UNKNOWN`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/orders/{id}/giftcard-usage":{"post":{"operationId":"upsertGiftcardUsage","x-vertical":"core","summary":"Mark a gift card unused, partially used, or fully used","description":"Upserts the `giftcard_usage` aux row for a gift-card order owned by the\nBearer (same auth as `GET /orders/{id}`). `partial` requires\n`amount_used_approx > 0`. Non-giftcard orders return `ORDER_NOT_GIFTCARD`.\n","tags":["Core — Orders"],"parameters":[{"name":"id","in":"path","required":true,"description":"Gift-card order ID","schema":{"type":"string"},"example":"6158845"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["unused","partial","full"]},"amount_used_approx":{"type":"number","description":"Required when status is partial"}}},"example":{"status":"partial","amount_used_approx":10}}}},"responses":{"200":{"description":"Updated giftcard_usage","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"giftcard_usage":{"status":"partial","amount_used_approx":10,"currency":"EUR","updated_at":"2026-09-16T18:00:00Z"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/orders/checkout/activate":{"post":{"operationId":"activateEsimCheckout","x-vertical":"core","summary":"Activate a paid eSIM from a checkout hash (native preferred)","description":"Hash-holder claim for a paid eSIM waiting on Activate. Body:\n`checkout_hash` (or `payment_link`) plus optional `lang` /\n`source=ios_app|android_app`. Same claim + detail payload as\n`POST /esim/orders/{id}/activate`, including additive\n`activation.result` (`ready` | `provisioning` | `failed`).\n`lang` is `es` or `en`. Rate-limited (5/min/IP, 10/hour/ticket).\nNo MCP tool.\n","tags":["Core — Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"checkout_hash":{"type":"string"},"payment_link":{"type":"string"},"lang":{"type":"string","enum":["en","es"],"default":"en"},"source":{"type":"string","enum":["ios_app","android_app","account"]}}},"example":{"checkout_hash":"e66d3fbdbebe0123456789abcdef0123","lang":"es","source":"ios_app"}}}},"responses":{"200":{"description":"Order-detail payload after claim","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"Claim blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/guest-lookup":{"post":{"operationId":"guestLookup","x-vertical":"core","summary":"Look up an order as a guest with email + ticket number","description":"Unauthenticated status for the native pending-order screen when\n`checkout_hash` is gone. Body: `email` + `order_id` (user-visible\nZendesk ticket only — not internal `paypal_ipn_orders.id`). Returns the same\nOSL payload as `GET /orders/{id}` including `tanteo`,\n`identity_url`, `actions[]`, and `refunded`.\n\nWrong email or wrong id both return generic `404 ORDER_NOT_FOUND`\n(no enumeration). Rate-limited: 5/min/IP and 10/hour/email\n(`429 TOO_MANY_ATTEMPTS`, `Retry-After`). PRO emails return\n`403 ACCOUNT_IS_PRO`. Sequential `GET /orders/{id}` still requires\nAuthorization.\n","tags":["Core — Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","order_id"],"properties":{"email":{"type":"string","format":"email"},"order_id":{"type":"string","description":"User-visible ticket number (Zendesk / paypal_ipn_orders.ticket_id only)"},"lang":{"type":"string","enum":["en","es"],"default":"en"}}},"example":{"email":"buyer@example.com","order_id":"8299001","lang":"en"}}}},"responses":{"200":{"description":"Same order-status payload as GET /orders/{id}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"description":"ACCOUNT_IS_PRO — guest tooling is not allowed for this email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders/lookup-guest":{"post":{"operationId":"guestLookupAlias","x-vertical":"core","x-alias-of":"guestLookup","summary":"Alias of POST /orders/guest-lookup","description":"Compatibility alias of `POST /orders/guest-lookup`.","tags":["Core — Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","order_id"],"properties":{"email":{"type":"string"},"order_id":{"type":"string"},"lang":{"type":"string"}}}}}},"responses":{"200":{"description":"Same as guestLookup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/orders/tanteo/validate":{"post":{"operationId":"validateTanteo","x-vertical":"core","summary":"Validate the 4-digit doctorSIM card-descriptor (tanteo)","description":"Native path for tickets waiting on the bank/card statement descriptor\n(last 4 digits of the doctorSIM charge — not an SMS code). Guest\ncapability is the unguessable `tanteo.token` from GET order status.\nBearer titular, when present, must match the token owner.\n","tags":["Core — Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"},"code":{"type":"string"},"help":{"type":"boolean"},"order_id":{"type":"string"},"lang":{"type":"string"}}},"example":{"token":"send_data_token","code":"1234"}}}},"responses":{"200":{"description":"Validated or help escalated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/balance":{"get":{"operationId":"getBalance","x-vertical":"core","summary":"Check credit balance","description":"Returns the current credit balance for the authenticated account. Requires\n`balance:read` (OAuth).\n\n**Sandbox** (`test_` key / OAuth `env: sandbox`): returns a **virtual** balance\n(`simulated: true`, `environment: sandbox`). It never changes because sandbox\norders never debit credits. Use it to validate parsing; read the real PRO\nbalance with a `live_` key or a production OAuth token.\n","tags":["Core — Balance"],"responses":{"200":{"description":"Current balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"examples":{"production":{"summary":"Production (live_ key / OAuth env production)","value":{"success":true,"data":{"balance":"150.00","currency":"EUR","environment":"production"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}},"sandbox":{"summary":"Sandbox (test_ key / OAuth env sandbox) — virtual balance","value":{"success":true,"data":{"balance":"1000.00","currency":"EUR","environment":"sandbox","simulated":true,"note":"Sandbox balance is virtual and never changes. Sandbox orders do not debit credits; use a live_ key to read the real PRO balance."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T12:00:00Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"OAuth token missing `balance:read` (`INSUFFICIENT_SCOPE`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/balance/history":{"get":{"operationId":"getBalanceHistory","x-vertical":"core","summary":"Credit transaction history","description":"Returns recent credit transactions (purchases and deductions). Requires\n`balance:read` (OAuth). Debits for PRO orders are negative lines; credit\nreturned for a refunded order appears as a separate positive line (the\norder's original `credits_used` is never rewritten).\n\n**Sandbox** (`test_` key / OAuth `env: sandbox`): one line per sandbox order\nwith `amount: \"0.00\"` (nothing is debited), `category: sandbox`,\n`order_id`, and `simulated_amount` = minus the gross amount a live key would\nhave debited (`-simulated_credits_used`, i.e. preview `total_cost_eur` incl.\nservice fee and SMS fee).\n","tags":["Core — Balance"],"parameters":[{"name":"limit","in":"query","description":"Max results (1-100)","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Transaction history","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"examples":{"production":{"summary":"Production (live_ key / OAuth env production)","value":{"success":true,"data":[{"id":12345,"amount":"-1.66","description":"Topup order #6158845","date":"2026-03-24 12:00:00"},{"id":12340,"amount":"100.00","description":"Credit purchase","date":"2026-03-20 09:30:00"}],"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z","count":2}}},"sandbox":{"summary":"Sandbox (test_ key / OAuth env sandbox)","value":{"success":true,"data":[{"id":null,"order_id":"sandbox_01ca75fd73c6202d","amount":"0.00","simulated_amount":"-1.97","description":"Sandbox order sandbox_01ca75fd73c6202d (fulfilled, no debit)","date":"2026-10-09T09:30:12Z","type":null,"category":"sandbox","category_label":"Sandbox order"}],"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-10-09T12:00:00Z","count":1,"environment":"sandbox"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhooks":{"post":{"operationId":"createWebhook","x-vertical":"core","summary":"Request a webhook (staff approval required)","description":"Request a webhook URL for order events. The row is stored as pending_approval and\nreturns HTTP 202. No event is queued or delivered until a doctorSIM operator approves it.\nMaximum 5 webhooks per account. The webhook secret is returned only once in the response -\nstore it securely for HMAC signature verification. Accounts flagged as fraudulent,\nand accounts that fail the PRO spend checks, are rejected with HTTP 403 and are not queued.\n","tags":["Core — Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"},"example":{"url":"https://example.com/webhooks/doctorsim","events":["order.created","order.fulfilled","order.failed"],"secret":"my_webhook_secret_123"}}}},"responses":{"202":{"description":"Webhook request accepted and waiting for staff approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"id":42,"request_id":42,"url":"https://example.com/webhooks/doctorsim","events":["order.created","order.fulfilled","order.failed"],"secret":"my_webhook_secret_123","status":"pending_approval","next_step":"A doctorSIM operator must approve this webhook before any event is queued or delivered.","note":"Save the secret now - it will not be shown again."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Write scope required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Maximum webhook limit reached (5 per account)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"operationId":"listWebhooks","x-vertical":"core","summary":"List webhooks","description":"Returns all webhook subscriptions for the authenticated account.","tags":["Core — Webhooks"],"responses":{"200":{"description":"List of webhooks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":[{"id":42,"url":"https://example.com/webhooks/doctorsim","events":["order.created","order.fulfilled"],"status":"active","failure_count":0,"last_triggered_at":null,"created_at":"2026-03-24 12:00:00"}],"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/webhooks/{id}":{"get":{"operationId":"getWebhook","x-vertical":"core","summary":"Get webhook detail","description":"Returns a single webhook subscription by ID.","tags":["Core — Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"Webhook detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"patch":{"operationId":"updateWebhook","x-vertical":"core","summary":"Update a webhook","description":"Update a webhook's URL, events, or status. Send only the fields you want to change.\nChanging the URL or the event list returns HTTP 202 and sets status back to pending_approval.\nSetting status to \"active\" is allowed only after staff approval and resets the failure counter.\nIt does not approve the webhook.\n","tags":["Core — Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook ID","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"},"example":{"events":["order.fulfilled","order.failed"],"status":"active"}}}},"responses":{"200":{"description":"Webhook updated (pause or resume of an already approved webhook)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"202":{"description":"URL or events changed. The webhook is pending_approval again and is not delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Write scope required, account not eligible, or webhook is not approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"deleteWebhook","x-vertical":"core","summary":"Delete a webhook","description":"Permanently removes a webhook subscription.","tags":["Core — Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"description":"Webhook ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"Webhook deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"id":42,"deleted":true},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Write scope required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/geo":{"get":{"operationId":"getGeo","x-vertical":"core","summary":"IP geolocation","description":"Returns the detected country based on the caller's IP address. Useful for pre-selecting the country in a UI.","tags":["Core — Status & utilities"],"responses":{"200":{"description":"Geolocation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"country_code":"es","ip":"203.0.113.42"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/reviews":{"get":{"operationId":"listReviews","x-vertical":"core","summary":"Trustpilot homepage reviews","description":"Visible 4–5 star Trustpilot reviews for the homepage, in the requested language.\nSource is the same `trustpilot_reviews_cache` table as the www static widget.\nLanguage: `?lang=` (es, en, es-ES, …), then `Accept-Language`, then `titulares.lang`, then English.\nFalls back to English when the locale has no rows. Optional `limit` 1–12 (default 6).\nPublic homepage data (no guest-catalog flag). Bearer is optional and only used to read `titulares.lang`.\n","tags":["Core — Status & utilities"],"parameters":[{"name":"lang","in":"query","description":"Locale (es, en, es-ES, fr, …). Trustpilot text is two-letter (es-ES → es).","schema":{"type":"string"},"example":"es-ES"},{"name":"limit","in":"query","description":"Max reviews (1-12)","schema":{"type":"integer","minimum":1,"maximum":12,"default":6}}],"responses":{"200":{"description":"Reviews + aggregate stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"reviews":[{"id":"62eb28378000af4a88540306","author":"Ana M.","rating":5,"title":"Recarga instantánea","text":"Llegó en segundos a un Telcel de México.","date":"2026-03-01T12:00:00Z","locale":"es","country":"MX","url":"https://es.trustpilot.com/reviews/62eb28378000af4a88540306","avatar":null}],"stats":{"locale":"es","total_reviews":12400,"trust_score":4.7,"stars_avg":4.7,"score_band":"excellent"},"profile_url":"https://es.trustpilot.com/review/doctorsim.com"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z","lang":"es","lang_requested":"es-ES","fallback":false,"limit":6,"count":1}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/countries":{"get":{"operationId":"listCountries","x-vertical":"topup","x-alias-of":"listTopupCountries","summary":"Alias of GET /topup/countries","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nReturns every country for any vertical (unfiltered). New integrations should call\n`GET /topup/countries` or `GET /giftcards/countries`. Same response shape.\nGuest responses send public cache headers + ETag.\n","tags":["Top-up"],"responses":{"200":{"description":"Same response as `GET /topup/countries`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/operators":{"get":{"operationId":"listOperators","x-vertical":"topup","x-alias-of":"listTopupOperators","summary":"Alias of GET /topup/operators","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/operators`. Same parameters and\nresponse as the canonical operation. `?country=` accepts ISO-2 (ES) or\nnumeric `id_country`. Guest responses send public cache headers + ETag.\n","tags":["Top-up"],"parameters":[{"name":"country","in":"query","description":"Filter by id_country (from /countries) or ISO-2 code (ES). Without this param the full catalog is returned.","schema":{"type":"string"},"example":"ES"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}}],"responses":{"200":{"description":"Same response as `GET /topup/operators`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/operators/{id}/service-types":{"get":{"operationId":"getOperatorServiceTypes","x-vertical":"topup","x-alias-of":"getTopupOperatorServiceTypes","summary":"Alias of GET /topup/operators/{id}/service-types","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/operators/{id}/service-types`. Same parameters and\nresponse as the canonical operation.\n","tags":["Top-up"],"parameters":[{"name":"id","in":"path","required":true,"description":"Operator ID","schema":{"type":"string"},"example":"5123"}],"responses":{"200":{"description":"Same response as `GET /topup/operators/{id}/service-types`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/operators/{id}/rates":{"get":{"operationId":"getOperatorRates","x-vertical":"topup","x-alias-of":"getTopupOperatorRates","summary":"Alias of GET /topup/operators/{id}/rates","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/operators/{id}/rates`. Same parameters and\nresponse as the canonical operation.\n","tags":["Top-up"],"parameters":[{"name":"id","in":"path","required":true,"description":"Operator ID (can be parent or child from service-types)","schema":{"type":"string"},"example":"5123"},{"name":"lang","in":"query","description":"Response language","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"q","in":"query","description":"Keyword search on product name and description (bundle/data features)","schema":{"type":"string"},"example":"5GB whatsapp"},{"name":"service_type","in":"query","description":"Filter by topup_type ID (from `/topup/operators/{id}/service-types`)","schema":{"type":"string"}}],"responses":{"200":{"description":"Same response as `GET /topup/operators/{id}/rates`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/service-types":{"get":{"operationId":"listServiceTypes","x-vertical":"topup","x-alias-of":"listTopupServiceTypes","summary":"Alias of GET /topup/service-types","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/service-types`. Same parameters and\nresponse as the canonical operation.\n","tags":["Top-up"],"responses":{"200":{"description":"Same response as `GET /topup/service-types`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/promotions":{"get":{"operationId":"listPromotions","x-vertical":"topup","x-alias-of":"listTopupPromotions","summary":"Alias of GET /topup/promotions","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/promotions`. Same parameters and\nresponse as the canonical operation.\n","tags":["Top-up"],"parameters":[{"name":"country","in":"query","required":true,"description":"Country ID","schema":{"type":"string"},"example":"34"}],"responses":{"200":{"description":"Same response as `GET /topup/promotions`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/carriers/lookup/{phone}":{"get":{"operationId":"carrierLookup","x-vertical":"topup","x-alias-of":"topupCarrierLookup","summary":"Alias of GET /topup/carriers/lookup/{phone}","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/carriers/lookup/{phone}`. Same parameters and\nresponse as the canonical operation.\n","tags":["Top-up"],"parameters":[{"name":"phone","in":"path","required":true,"description":"Phone number in E.164 format","schema":{"type":"string"},"example":"+522212345678"}],"responses":{"200":{"description":"Same response as `GET /topup/carriers/lookup/{phone}`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/products/search":{"get":{"operationId":"searchProducts","x-vertical":"topup","x-alias-of":"searchTopupProducts","summary":"Alias of GET /topup/products/search","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nUnfiltered catalog search (top-up operators and gift card brands together). New\nintegrations should call `GET /topup/products/search` or `GET /giftcards/brands`.\n","tags":["Top-up"],"parameters":[{"name":"country","in":"query","required":true,"description":"ISO 3166-1 alpha-2 code (for example \"es\") or a numeric id_pais (1-6 digits). Any other value is rejected with 400.","schema":{"type":"string"},"example":"es"},{"name":"operator_id","in":"query","description":"Numeric operator id (`id_operator`). Non-numeric values are rejected with 400.","schema":{"type":"string"},"example":"13978"},{"name":"service_type","in":"query","description":"Numeric topup_type id (from `/service-types`). Non-numeric values are rejected with 400.","schema":{"type":"string"}},{"name":"q","in":"query","description":"Catalog keyword, matched as a literal substring (max 100 characters). With operator_id, searches bundle/data descriptions in memory.","schema":{"type":"string"}},{"name":"lang","in":"query","description":"Response language (rate description search mode)","schema":{"type":"string","enum":["en","es"],"default":"en"}},{"name":"limit","in":"query","description":"Page size. Digits only; values outside 1-100 are clamped. Non-numeric values are rejected with 400. Default 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Keyset pagination cursor (`meta.cursor` from the previous page). Must be digits. An invalid cursor is rejected with 400.","schema":{"type":"string"}}],"responses":{"200":{"description":"Same response as `GET /topup/products/search`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/products/{operator_id}":{"get":{"operationId":"getProductDetail","x-vertical":"topup","x-alias-of":"getTopupProductDetail","summary":"Alias of GET /topup/products/{operator_id}","description":"Un-prefixed alias kept for the Rork app and existing MCP connectors.\nNew integrations should call `GET /topup/products/{operator_id}`. Same parameters and\nresponse as the canonical operation.\n","tags":["Top-up"],"parameters":[{"name":"operator_id","in":"path","required":true,"description":"Operator ID (`id_operator`)","schema":{"type":"string"},"example":"5123"},{"name":"lang","in":"query","description":"Response language for descriptions","schema":{"type":"string","enum":["en","es"],"default":"en"}}],"responses":{"200":{"description":"Same response as `GET /topup/products/{operator_id}`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/auth/confirm":{"post":{"operationId":"nativeAuthConfirm","x-vertical":"native-auth","summary":"Confirm or inspect an activation / reset token","description":"Official mobile app. Token is the 64-hex `titulares.activationseed` from the\nemail deep link query `v`. Send `titular_id` (query `i`) or `email` with it.\nA token alone is `401 INVALID_TOKEN`. The token expires 7 days after it is issued.\nWithout `password`: inspect only — `{status:needs_password, next_step:set_password, purpose:activate|reset}`.\nWith `password`: consume the one-time token, activate or reset.\nA signup confirmed within 7 days returns a native session. An older never-activated\naccount returns `{next_step:login}` and the caller signs in with the new password.\nSame INVALID_TOKEN for every miss (no account enumeration). Rate-limited.\n","tags":["Native App Auth"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"64-hex activation seed (`v` on the confirm link). Expires 7 days after issue.","example":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"titular_id":{"type":"integer","description":"Account id from the email `i` query. Required when `email` is omitted.","example":123456},"email":{"type":"string","description":"Account email. Required when `titular_id` is omitted.","example":"user@example.com"},"password":{"type":"string","description":"Optional. When set, completes confirm+set in one call. New passwords are 8-50 characters. Login of a shorter existing password is unchanged.","minLength":8,"maxLength":50}}},"examples":{"inspect":{"summary":"Inspect token from a deep link","value":{"token":"a1b2c3d4e5f60718293a4b5c6d7e8f90","titular_id":123456}},"confirmAndSet":{"summary":"Activate and set password","value":{"token":"a1b2c3d4e5f60718293a4b5c6d7e8f90","titular_id":123456,"password":"new-pass-here"}}}}}},"responses":{"200":{"description":"Inspect next-step or session after set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"examples":{"needsPassword":{"summary":"Inspect (no password yet)","value":{"success":true,"data":{"status":"needs_password","next_step":"set_password","purpose":"activate","token_valid":true,"titular_id":123456},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-09-23T12:00:00Z"}}},"session":{"summary":"Confirm with password","value":{"success":true,"data":{"tokens":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","token_type":"Bearer","expires_in":3600,"refresh_token":"rt_…","refresh_expires_in":2592000},"titular":{"id":123456,"email":"user@example.com","first_name":"Ada","last_name":"Lovelace","is_pro":false,"checkout_mode":"payment_link","lang":"en","environment":"production"},"environment":"production","checkout_mode":"payment_link","next_step":"done","purpose":"activate"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-09-23T12:00:00Z"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Invalid, expired, or already-used token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"INVALID_TOKEN","message":"The confirmation token is invalid, expired or already used.","next_step":"Ask the user for a fresh email link, or request a new reset."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-09-23T12:00:00Z"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/auth/password/set":{"post":{"operationId":"nativeAuthPasswordSet","x-vertical":"native-auth","summary":"Set password with a one-time email token","description":"Official mobile app.         Completes signup activation or password reset after\n`POST /v2/auth/password/forgot`. Token is the 64-hex `v` from the email,\nplus `titular_id` or `email`. A token alone is `401 INVALID_TOKEN`.\nThe token expires 7 days after it is issued and is rotated on success.\nA fresh signup returns a session. An older never-activated account\nreturns `{next_step:login}` and the caller signs in with the new password.\n","tags":["Native App Auth"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string","description":"64-hex seed. Expires 7 days after issue.","example":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"titular_id":{"type":"integer","description":"Required when `email` is omitted.","example":123456},"email":{"type":"string","description":"Required when `titular_id` is omitted.","example":"user@example.com"},"password":{"type":"string","minLength":8,"maxLength":50,"description":"New password. 8-50 characters. Does not apply to login.","example":"new-pass-here"}}},"example":{"token":"a1b2c3d4e5f60718293a4b5c6d7e8f90","titular_id":123456,"password":"new-pass-here"}}}},"responses":{"200":{"description":"Session issued after one-time set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"tokens":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","token_type":"Bearer","expires_in":3600,"refresh_token":"rt_…","refresh_expires_in":2592000},"titular":{"id":123456,"email":"user@example.com","first_name":"Ada","last_name":"Lovelace","is_pro":false,"checkout_mode":"payment_link","lang":"en","environment":"production"},"environment":"production","checkout_mode":"payment_link","next_step":"done","purpose":"reset"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-09-23T12:00:00Z"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Invalid or already-used token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"INVALID_TOKEN","message":"The confirmation token is invalid, expired or already used.","next_step":"Ask the user for a fresh email link, or request a new reset."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-09-23T12:00:00Z"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/auth/password/change":{"post":{"operationId":"nativeAuthPasswordChange","x-vertical":"native-auth","summary":"Change password (logged-in or reset-token)","description":"Official mobile app. Two mutually exclusive modes:\n1) `Authorization: Bearer` + `current_password` + `new_password` — verifies the\n   current password, updates it, revokes other sessions, returns a fresh pair.\n2) No Bearer + `token` + `titular_id` or `email` + `new_password` — same one-time reset path as `/auth/password/set`. A token alone is `401 INVALID_TOKEN`.\nFacebook is not supported.\n","tags":["Native App Auth"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"current_password":{"type":"string"},"new_password":{"type":"string","minLength":8,"maxLength":50,"description":"New password. 8-50 characters. The current password is not length-checked."},"token":{"type":"string"},"titular_id":{"type":"integer"}}},"examples":{"loggedIn":{"summary":"Signed-in change","value":{"current_password":"old-pass-here","new_password":"new-pass-here"}},"resetToken":{"summary":"Reset-token path","value":{"token":"a1b2c3d4e5f60718293a4b5c6d7e8f90","titular_id":123456,"new_password":"new-pass-here"}}}}}},"responses":{"200":{"description":"Fresh session after change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"},"example":{"success":true,"data":{"tokens":{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","token_type":"Bearer","expires_in":3600,"refresh_token":"rt_…","refresh_expires_in":2592000},"titular":{"id":123456,"email":"user@example.com","first_name":"Ada","last_name":"Lovelace","is_pro":false,"checkout_mode":"payment_link","lang":"en","environment":"production"},"environment":"production","checkout_mode":"payment_link","next_step":"done","purpose":"change"},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-09-23T12:00:00Z"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/mpp/orders":{"post":{"operationId":"discoverMppOrder","x-discovery-rail":"mpp","summary":"Place a paid order (MPP Stripe charge)","description":"Discovery alias of POST https://api.doctorsim.com/v2/mpp/orders. Unpaid calls are answered on the API host. This path does not change REST POST /orders.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live MPP base","url":"https://api.doctorsim.com/v2/mpp"},"x-payment-info":{"offers":[{"amount":null,"currency":"eur","description":"Mobile top-up, gift card, or travel eSIM order (amount varies by product)","intent":"charge","method":"stripe"}],"price":{"mode":"dynamic","currency":"EUR","min":"0.50","max":"1000.00"},"protocols":[{"mpp":{"method":"stripe","intent":"charge","currency":"eur"}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/mpp/orders/topup":{"post":{"operationId":"discoverMppTopupOrder","x-discovery-rail":"mpp","summary":"Buy mobile top-up (MPP)","description":"Discovery alias of POST https://api.doctorsim.com/v2/mpp/orders/topup. Unpaid calls are answered on the API host.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live MPP base","url":"https://api.doctorsim.com/v2/mpp"},"x-payment-info":{"offers":[{"amount":null,"currency":"eur","description":"Prepaid mobile top-up (amount from price_token)","intent":"charge","method":"stripe"}],"price":{"mode":"dynamic","currency":"EUR","min":"0.50","max":"1000.00"},"protocols":[{"mpp":{"method":"stripe","intent":"charge","currency":"eur"}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/mpp/orders/esim":{"post":{"operationId":"discoverMppEsimOrder","x-discovery-rail":"mpp","summary":"Buy travel eSIM (MPP)","description":"Discovery alias of POST https://api.doctorsim.com/v2/mpp/orders/esim. Unpaid calls are answered on the API host.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live MPP base","url":"https://api.doctorsim.com/v2/mpp"},"x-payment-info":{"offers":[{"amount":null,"currency":"eur","description":"Travel eSIM order (amount from price_token)","intent":"charge","method":"stripe"}],"price":{"mode":"dynamic","currency":"EUR","min":"0.50","max":"1000.00"},"protocols":[{"mpp":{"method":"stripe","intent":"charge","currency":"eur"}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/mpp/orders/giftcard":{"post":{"operationId":"discoverMppGiftcardOrder","x-discovery-rail":"mpp","summary":"Buy a digital gift card (MPP)","description":"Discovery alias of POST https://api.doctorsim.com/v2/mpp/orders/giftcard. Unpaid calls are answered on the API host.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live MPP base","url":"https://api.doctorsim.com/v2/mpp"},"x-payment-info":{"offers":[{"amount":null,"currency":"eur","description":"Digital gift card order (amount from price_token)","intent":"charge","method":"stripe"}],"price":{"mode":"dynamic","currency":"EUR","min":"0.50","max":"1000.00"},"protocols":[{"mpp":{"method":"stripe","intent":"charge","currency":"eur"}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/x402/orders":{"post":{"operationId":"discoverX402Order","x-discovery-rail":"x402","summary":"Place a paid order (x402)","description":"Discovery alias of POST https://api.doctorsim.com/v2/x402/orders. The x402 rail already exists on the API host. This path does not change HTTP 402 behavior.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live x402 base","url":"https://api.doctorsim.com/v2/x402"},"x-payment-info":{"offers":[{"amount":null,"currency":"usd","description":"Mobile top-up, gift card, or travel eSIM order (amount from price_token)","intent":"charge","method":"x402"}],"price":{"mode":"dynamic","currency":"USD","min":"0.50","max":"1000.00"},"protocols":[{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/x402/orders/topup":{"post":{"operationId":"discoverX402TopupOrder","x-discovery-rail":"x402","summary":"Buy mobile top-up (x402)","description":"Discovery alias of POST https://api.doctorsim.com/v2/x402/orders/topup.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live x402 base","url":"https://api.doctorsim.com/v2/x402"},"x-payment-info":{"offers":[{"amount":null,"currency":"usd","description":"Prepaid mobile top-up (amount from price_token)","intent":"charge","method":"x402"}],"price":{"mode":"dynamic","currency":"USD","min":"0.50","max":"1000.00"},"protocols":[{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/x402/orders/esim":{"post":{"operationId":"discoverX402EsimOrder","x-discovery-rail":"x402","summary":"Buy travel eSIM (x402)","description":"Discovery alias of POST https://api.doctorsim.com/v2/x402/orders/esim.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live x402 base","url":"https://api.doctorsim.com/v2/x402"},"x-payment-info":{"offers":[{"amount":null,"currency":"usd","description":"Travel eSIM order (amount from price_token)","intent":"charge","method":"x402"}],"price":{"mode":"dynamic","currency":"USD","min":"0.50","max":"1000.00"},"protocols":[{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}},"/x402/orders/giftcard":{"post":{"operationId":"discoverX402GiftcardOrder","x-discovery-rail":"x402","summary":"Buy a digital gift card (x402)","description":"Discovery alias of POST https://api.doctorsim.com/v2/x402/orders/giftcard.","tags":["Core — Orders"],"security":[],"externalDocs":{"description":"Live x402 base","url":"https://api.doctorsim.com/v2/x402"},"x-payment-info":{"offers":[{"amount":null,"currency":"usd","description":"Digital gift card order (amount from price_token)","intent":"charge","method":"x402"}],"price":{"mode":"dynamic","currency":"USD","min":"0.50","max":"1000.00"},"protocols":[{"x402":{}}]},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["price_token"],"properties":{"price_token":{"type":"string"}}}}}},"responses":{"201":{"description":"Order created after successful payment","content":{"application/json":{"schema":{"type":"object"}}}},"402":{"description":"Payment Required"}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"PRO API key authentication. The Bearer token **must** contain a colon separating\n`api_id` and `api_secret` (this distinguishes API keys from OAuth JWTs).\n\nProduction: `Authorization: Bearer live_abc123def456789a:sk_789ghi012jkl345...`\n\nSandbox: `Authorization: Bearer test_abc123def456789a:sk_789ghi012jkl345...`\n"},"OAuthBearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"OAuth 2.0 access token (RS256 JWT) from `POST /oauth/token` on the authorization server.\n\nExample: `Authorization: Bearer eyJhbGciOiJSUzI1NiIs...`\n\nDiscovery: https://www.doctorsim.com/.well-known/oauth-authorization-server\n"},"OAuth2":{"type":"oauth2","description":"OAuth 2.0 authorization server at www.doctorsim.com.\nDynamic client registration: POST https://www.doctorsim.com/oauth/register\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.doctorsim.com/oauth/authorize","tokenUrl":"https://www.doctorsim.com/oauth/token","scopes":{"orders":"write: Place orders","payment_link":"Hosted checkout link for regular accounts","balance":"read: Read PRO credit balance","webhooks":"write: Create and manage webhooks (PRO after API approval)"}},"clientCredentials":{"tokenUrl":"https://www.doctorsim.com/oauth/token","scopes":{"orders":"write: Place orders","payment_link":"Hosted checkout link for regular accounts","balance":"read: Read PRO credit balance","webhooks":"write: Manage webhooks (PRO after API approval)"}}}}},"schemas":{"SuccessResponse":{"type":"object","required":["success","data","meta"],"properties":{"success":{"type":"boolean","const":true},"data":{"description":"Response payload (type varies by endpoint)"},"meta":{"$ref":"#/components/schemas/Meta"}}},"ErrorResponse":{"type":"object","required":["success","data","error","meta"],"properties":{"success":{"type":"boolean","const":false},"data":{"type":"null"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code","example":"INVALID_PHONE_NUMBER"},"message":{"type":"string","description":"Human-readable error description","example":"The phone number format is invalid."},"next_step":{"type":"string","description":"Actionable hint for agents (link account, retry, pick another plan)","example":"Retry with E.164 or call lookup_carrier."},"client_ip":{"type":"string","description":"Trusted caller IP when the error is IP_ALLOWLIST_REQUIRED or IP_NOT_ALLOWED","example":"203.0.113.50"}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"Meta":{"type":"object","properties":{"request_id":{"type":"string","description":"Unique request identifier for support reference","example":"req_abc12345-6789-abcd-ef01-234567890abc"},"timestamp":{"type":"string","format":"date-time","description":"UTC timestamp of the response","example":"2026-03-24T12:00:00Z"},"cursor":{"type":"string","description":"Pagination cursor for next page (present on list endpoints)"},"has_more":{"type":"boolean","description":"Whether more pages exist"},"total":{"type":"integer","description":"Total count of items (when computed)"}}},"NativeCampaignAttribution":{"type":"object","description":"Optional campaign contract for the native iOS/Android apps and any\nclient that already has web GET click ids. Same names as storefront\nquery params. Persisted on create (`checkout_hash` store + titular\nclick ids when logged in) and copied onto hosted `payment_link`.\nPreview accepts these fields without persisting. `asa_attribution_token`\nis store-only (no Apple Ads fetch). Do not send advertising IDs.\n","properties":{"utm_source":{"type":"string","maxLength":128,"example":"google"},"utm_medium":{"type":"string","maxLength":128,"example":"cpc"},"utm_campaign":{"type":"string","maxLength":128,"example":"rork_ios_spring"},"utm_content":{"type":"string","maxLength":128,"example":"banner_a"},"utm_term":{"type":"string","maxLength":128,"example":"mexico telcel"},"gclid":{"type":"string","maxLength":255},"gbraid":{"type":"string","maxLength":255,"description":"Google iOS app-to-web click id"},"wbraid":{"type":"string","maxLength":255,"description":"Google web-to-app click id"},"fbclid":{"type":"string","maxLength":255},"ttclid":{"type":"string","maxLength":255},"twclid":{"type":"string","maxLength":255},"sccid":{"type":"string","maxLength":255},"cio_link_id":{"type":"string","maxLength":512,"description":"Email click token. Persisted on the order; optional click ping stays on the web path."},"asa_attribution_token":{"type":"string","maxLength":2048,"description":"Store-only stub. Native client fetch is a later phase."}}},"CreateOrderRequest":{"oneOf":[{"$ref":"#/components/schemas/CreateOrderWithPriceTokenRequest"},{"$ref":"#/components/schemas/EsimCreateOrderRequest"},{"$ref":"#/components/schemas/CreateOrderLegacyRequest"}],"description":"One request schema for every vertical. Prefer `CreateOrderWithPriceTokenRequest`\n(top-up / gift card) or `EsimCreateOrderRequest` (travel eSIM; selected by\n`type: esim`, an eSIM `price_token`, or `catalog_id`). Legacy fields are\naccepted when `price_token` is omitted. Optional `NativeCampaignAttribution`\nfields are accepted on every variant.\n"},"EsimCreateOrderRequest":{"type":"object","required":["catalog_id","price_token"],"properties":{"type":{"type":"string","enum":["esim"],"description":"Optional explicit vertical; inferred from `price_token` / `catalog_id` when omitted"},"catalog_id":{"type":"integer","description":"eSIM catalog ID from `/esim/products`","example":42},"price_token":{"type":"string","description":"Signed token from `/esim/products` or `/esim/products/{catalog_id}`","example":"esim__42__9.99__EUR__0__1710000000__a1b2c3d4e5f6g7h8"},"lang":{"type":"string","enum":["en","es"],"default":"en"},"currency":{"type":"string","enum":["EUR","USD"],"default":"EUR"},"idempotency_key":{"type":"string","minLength":1,"maxLength":128,"description":"Same as the Idempotency-Key header (header wins). Required for safe\nretries. Use a new key for each distinct SKU / denomination.\n"},"checkout_mode":{"type":"string","enum":["stripe_link"],"description":"Optional. Omit for the default doctorsim.com hash `payment_link` (D-1).\n`stripe_link` returns a Stripe Checkout Session URL when\n`agentic_stripe_link` is ON; otherwise `STRIPE_LINK_DISABLED`.\nPRO `credits` always wins.\n"},"utm_source":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/utm_source"},"utm_medium":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/utm_medium"},"utm_campaign":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/utm_campaign"},"utm_content":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/utm_content"},"utm_term":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/utm_term"},"gclid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/gclid"},"gbraid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/gbraid"},"wbraid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/wbraid"},"fbclid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/fbclid"},"ttclid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/ttclid"},"twclid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/twclid"},"sccid":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/sccid"},"cio_link_id":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/cio_link_id"},"asa_attribution_token":{"$ref":"#/components/schemas/NativeCampaignAttribution/properties/asa_attribution_token"}}},"CreateOrderWithPriceTokenRequest":{"type":"object","required":["price_token"],"properties":{"type":{"type":"string","enum":["topup","giftcard"],"description":"Optional explicit vertical; inferred from the rate token when omitted"},"price_token":{"type":"string","description":"Rate `token` from `GET /topup/operators/{id}/rates` or `GET /topup/products/{operator_id}`.\nEncodes product slug, tool_id, denomination_id, cost, and currency.\n","example":"mexico-telcel-20-mxn-3__2945583__35767699__1.00__EUR"},"phone":{"type":"string","description":"Recipient MSISDN in E.164 format. Required for mobile top-ups; omit for gift cards\nthat do not require a phone.\n","example":"+522212345678"},"operator_id":{"type":"string","description":"Optional cross-check — must match the token if supplied","example":"5123"},"country_id":{"type":"string","description":"Optional. Omit when using `price_token` — the token already encodes the destination\n(`id_country`, e.g. Spain=34). If sent, must be `id_country` from `GET /countries`\nor product search. Unknown invented IDs are ignored. A known country that does not\nmatch the token still fails.\n","example":"34"},"lang":{"type":"string","description":"Language for notifications","enum":["en","es"],"default":"en"},"sms_notification":{"type":"boolean","description":"Send an SMS notification to the recipient. When `true` a fixed SMS\nfee of 0.99 EUR (≈0.99 USD) is added to the order total.\n","default":false},"sms_text":{"type":"string","description":"Custom SMS text (max 140 chars). Only used when `sms_notification=true`.\nHTML tags and template markers are removed before the message is stored.\nIf `sms_notification=true` and this is omitted or empty, a default\nlocalized message (per `lang`) is sent automatically — the order does\nnot fail for a missing SMS text. Preview responses do not echo this field.\n","maxLength":140},"idempotency_key":{"type":"string","minLength":1,"maxLength":128,"description":"Same as the Idempotency-Key header (header wins). Use a new key per SKU."},"sandbox_outcome":{"type":"string","enum":["fulfilled","failed","cancelled","refunded"],"default":"fulfilled","description":"**Sandbox credentials only** (ignored in production). Terminal state the\nsandbox order reaches ~3 seconds after create. Without this field, a top-up\nphone ending in `0001` simulates `failed`, `0002` `cancelled`, `0003`\n`refunded`; any other number simulates `fulfilled`.\n"},"checkout_mode":{"type":"string","enum":["stripe_link"],"description":"Optional. Omit for the default doctorsim.com hash `payment_link`.\n`stripe_link` is Gate 4.3 opt-in (flag `agentic_stripe_link`).\n"},"utm_source":{"type":"string","maxLength":128},"utm_medium":{"type":"string","maxLength":128},"utm_campaign":{"type":"string","maxLength":128},"utm_content":{"type":"string","maxLength":128},"utm_term":{"type":"string","maxLength":128},"gclid":{"type":"string","maxLength":255},"gbraid":{"type":"string","maxLength":255},"wbraid":{"type":"string","maxLength":255},"fbclid":{"type":"string","maxLength":255},"ttclid":{"type":"string","maxLength":255},"twclid":{"type":"string","maxLength":255},"sccid":{"type":"string","maxLength":255},"cio_link_id":{"type":"string","maxLength":512},"asa_attribution_token":{"type":"string","maxLength":2048,"description":"Store-only stub. Native client fetch is a later phase."}}},"CreateOrderLegacyRequest":{"type":"object","required":["operator_id","country_id","tool_id","denomination_id","amount","currency"],"properties":{"operator_id":{"type":"string","description":"Operator ID from `/operators`","example":"5123"},"country_id":{"type":"string","description":"Country ID (`id_country` from `/countries`)","example":"1144"},"tool_id":{"type":"string","description":"Legacy provider tool ID (deprecated — use price_token)","example":"2945585"},"denomination_id":{"type":"string","description":"Legacy denomination ID (deprecated — use price_token)","example":"12806338"},"amount":{"type":"string","description":"Denomination receive amount matching the selected rate","example":"50"},"phone":{"type":"string","description":"Phone number to recharge (E.164). Required for mobile/airtime top-ups; omit for gift cards (delivered digitally).","example":"+521234567890"},"currency":{"type":"string","description":"Payment currency (EUR or USD)","enum":["EUR","USD"],"example":"EUR"},"lang":{"type":"string","enum":["en","es"],"default":"en"},"sms_notification":{"type":"boolean","default":false,"description":"Send an SMS notification to the recipient. When `true` a fixed SMS\nfee of 0.99 EUR (≈0.99 USD) is added to the order total.\n"},"sms_text":{"type":"string","maxLength":140,"description":"Custom SMS text. Only used when `sms_notification=true`. HTML tags\nand template markers are removed before the message is stored. If\nomitted while `sms_notification=true`, a default localized message\nis sent. Preview responses do not echo this field.\n"},"idempotency_key":{"type":"string","minLength":1,"maxLength":128,"description":"Same as the Idempotency-Key header (header wins). Use a new key per SKU."},"utm_source":{"type":"string","maxLength":128},"utm_medium":{"type":"string","maxLength":128},"utm_campaign":{"type":"string","maxLength":128},"utm_content":{"type":"string","maxLength":128},"utm_term":{"type":"string","maxLength":128},"gclid":{"type":"string","maxLength":255},"gbraid":{"type":"string","maxLength":255},"wbraid":{"type":"string","maxLength":255},"cio_link_id":{"type":"string","maxLength":512},"asa_attribution_token":{"type":"string","maxLength":2048}}},"RateItem":{"type":"object","description":"Single price point from `/operators/{id}/rates` or product detail","properties":{"token":{"type":"string","description":"Pass as `price_token` in POST /orders"},"product_slug":{"type":"string"},"product_name":{"type":"string"},"received_amount":{"type":"string"},"received_currency":{"type":"string"},"cost_eur":{"type":"string","description":"Recharge cost (face/catalog price) in EUR, excluding the service fee"},"cost_usd":{"type":"string","description":"Recharge cost in USD, excluding the service fee"},"service_fee_eur":{"type":"string","description":"Exact per-rate service fee in EUR for the authenticated account.\nHonors PRO/VIP tier overrides and country/operator exceptions. Only\npresent on authenticated calls; standard (non-PRO) callers receive a\nbest-effort standard fee.\n","example":"0.99"},"service_fee_usd":{"type":"string","description":"Exact per-rate service fee in USD (1:1 with EUR for the rounded fee amount).","example":"0.99"},"total_cost_eur":{"type":"string","description":"cost_eur + service_fee_eur — the full amount the recharge will cost in EUR.","example":"20.99"},"total_cost_usd":{"type":"string","description":"cost_usd + service_fee_usd — the full amount the recharge will cost in USD.","example":"22.99"},"topup_type":{"type":"integer"},"description":{"type":"string"},"operator":{"type":"string"},"country":{"type":"string"},"image":{"type":"string","format":"uri"}}},"CreateWebhookRequest":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","description":"HTTPS URL to receive webhook payloads","example":"https://example.com/webhooks/doctorsim"},"events":{"type":"array","items":{"type":"string","enum":["order.created","order.fulfilled","order.failed"]},"description":"List of event types to subscribe to","minItems":1},"secret":{"type":"string","description":"Signing secret for HMAC verification (auto-generated if omitted)","example":"my_webhook_secret_123"}}},"UpdateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"New HTTPS URL"},"events":{"type":"array","items":{"type":"string","enum":["order.created","order.fulfilled","order.failed"]},"minItems":1},"status":{"type":"string","enum":["active","paused"],"description":"Webhook status (setting to active resets failure count)"}}}},"responses":{"Unauthorized":{"description":"Authentication failed. No `Authorization` header is `MISSING_AUTH`.\nA present header that is not `Bearer` is `INVALID_AUTH_SCHEME`.\nInvalid API key, secret, or OAuth token use their own codes.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"missingAuth":{"summary":"No Authorization header","value":{"success":false,"data":null,"error":{"code":"MISSING_AUTH","message":"Authorization header is required."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}},"invalidAuthScheme":{"summary":"Authorization present but not Bearer","value":{"success":false,"data":null,"error":{"code":"INVALID_AUTH_SCHEME","message":"Authorization header must use Bearer scheme."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}},"invalidApiKey":{"summary":"Invalid API key","value":{"success":false,"data":null,"error":{"code":"INVALID_API_KEY","message":"The provided API key does not exist."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}},"invalidToken":{"summary":"Invalid OAuth JWT","value":{"success":false,"data":null,"error":{"code":"INVALID_TOKEN","message":"The OAuth access token is invalid or expired."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}}}},"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"MISSING_PARAMETER","message":"A required parameter is missing."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"The requested resource was not found."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}},"RateLimited":{"description":"Rate limit exceeded","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the rate limit resets"},"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Max requests per minute for your tier"},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Remaining requests in current window"},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp when the window resets"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"data":null,"error":{"code":"RATE_LIMIT_EXCEEDED","message":"Rate limit of 1000 requests per minute exceeded. Upgrade your plan for higher limits."},"meta":{"request_id":"req_abc12345-6789-abcd-ef01-234567890abc","timestamp":"2026-03-24T12:00:00Z"}}}}}}}}