{
  "openapi": "3.1.0",
  "info": {
    "title": "Customer API",
    "version": "2.0.0",
    "summary": "Virtual phone numbers for SMS verification: buy a number, receive the code, pay only when it arrives.",
    "description": "Buy a temporary phone number for a country and a service (WhatsApp, Telegram, …), receive the verification SMS on it, then finish or cancel the order. **You are billed only for orders that received an SMS.** Prices are in USD.\n\nAll endpoints are under `/api/v2/client` and return JSON.\n\n## Quick start\n\n```sh\nKEY=sk_...                                   # from your account manager\nAPI=https://<host>/api/v2/client\n\n# 1. What can I buy, and at what price?\ncurl -s $API/prices/vietnam -H \"Authorization: Bearer $KEY\"\n# [{\"country\":\"vietnam\",\"service\":\"wa\",\"price\":0.684,\"available\":240,...}]\n\n# 2. Buy a number. The Idempotency-Key makes a retried request safe.\ncurl -s $API/orders -H \"Authorization: Bearer $KEY\" \\\n  -H 'Content-Type: application/json' -H 'Idempotency-Key: 7d1f0c2e' \\\n  -d '{\"country\": \"vietnam\", \"service\": \"wa\", \"maxPrice\": 0.80}'\n# {\"id\":8816265315333972,\"status\":\"waiting\",\"phone\":\"84912340000\",...}\n\n# 3. Use the phone number on the service, then wait for the SMS\n#    (or let it be pushed to your webhook, see \"Webhook\").\ncurl -s $API/orders/8816265315333972 -H \"Authorization: Bearer $KEY\" -H 'Prefer: wait=10'\n# {\"status\":\"received\",\"code\":\"482913\",\"sms\":[{\"text\":\"Your WhatsApp code 482-913\",...}],...}\n\n# 4. Done with the number: finish it. (No SMS came? Cancel it instead: not billed.)\ncurl -s -X POST $API/orders/8816265315333972/finish -H \"Authorization: Bearer $KEY\"\n```\n\n## Authentication\n\nSend your API key on every request as `Authorization: Bearer sk_...` (or `X-API-Key: sk_...`). A missing or unknown key is `401 invalid_key`; a disabled account is `403 account_disabled`. Keep the key secret: anyone with it can buy numbers on your account. Ask your account manager to rotate it if it leaks; the old key stops working immediately.\n\n## Order lifecycle and billing\n\n| Status | Meaning | Billed |\n|---|---|---|\n| `waiting` | Number issued; no SMS yet. | Only if an SMS arrives |\n| `received` | At least one SMS arrived. More can still arrive. | Yes |\n| `completed` | You finished the order (or it finished itself, see below). | Yes |\n| `cancelled` | You cancelled before any SMS arrived. | No |\n| `expired` | No SMS arrived before `expiresAt` (20 minutes after purchase). | No |\n\n- **Finish** (`POST /orders/{id}/finish`) once you have the code. It is only allowed after an SMS.\n- **Cancel** (`POST /orders/{id}/cancel`) when the SMS isn't coming or the number didn't work. It is only allowed before any SMS, and costs nothing.\n- An order with an SMS that you never finish completes on its own at `expiresAt`; one without an SMS expires, unbilled.\n- `billed` on each order says whether it counts (or will count) towards what you pay: `true` unless the order was cancelled or expired without an SMS. `GET /me` shows the running total as `stats.spent`.\n- Your price is fixed when you buy: later price changes never affect an order you hold.\n\n## Getting the SMS: long-poll or webhook\n\nEither works; use whichever suits your system, or both.\n\n- **Long-poll**: `GET /orders/{id}` with `Prefer: wait=10` holds the request until an SMS arrives (or the order closes), up to 10 seconds, then returns the order. Repeat until `status` is no longer `waiting`.\n- **Webhook**: set a URL with `PUT /webhook`; every SMS is POSTed to it as an `sms.received` event, signed per [Standard Webhooks](https://www.standardwebhooks.com) and retried for up to about 32 hours. See the `sms.received` webhook for the payload and how to verify it.\n\nEvery SMS carries `code`: the verification code found in the text (a group of 4-8 digits, or `123-456` / `123 456` joined to `123456`), or `null` when none was found. The full `text` is always included.\n\n## Idempotency\n\nNetwork errors happen. To retry a purchase safely, send a unique `Idempotency-Key` header (up to 128 characters, e.g. a UUID) with `POST /orders`. Repeating a request with the same key returns the order the first one created (`200` instead of `201`) and never buys a second number. Keys are per account and never expire.\n\n## Errors\n\nEvery error, for every status code, has the same shape:\n\n```json\n{\"error\": {\"code\": \"no_numbers\", \"message\": \"no numbers available right now\", \"details\": {\"suppliersTried\": 2}}}\n```\n\nBranch on `code` (stable), not on `message` (for people; may change). `details` is present only for some codes.\n\n| HTTP | `code` | When | What to do |\n|---|---|---|---|\n| 400 | `invalid_request` | Malformed JSON, unknown field, bad date, `maxPrice` ≤ 0 | Fix the request; `message` says what is wrong |\n| 401 | `invalid_key` | Missing or unknown API key | Check the `Authorization` header |\n| 403 | `account_disabled` | Your account is disabled | Contact your account manager |\n| 404 | `unknown_route` | That country + service isn't for sale | Check `GET /prices` |\n| 404 | `not_found` | No such order (or not yours), or no such path | Check the id or path |\n| 405 | `method_not_allowed` | Wrong HTTP method | Use one of those in `Allow` |\n| 408 | `request_timeout` | The request took over 15 s | Retry |\n| 409 | `no_numbers` | Nothing in stock at or below the price right now (`details.suppliersTried`) | Retry later or try another country |\n| 409 | `price_above_max` | The price is above your `maxPrice` (`details.price`) | Raise `maxPrice` or try later |\n| 409 | `not_priced` | No price can be set for this route right now | Retry later |\n| 409 | `invalid_state` | E.g. cancelling after an SMS, finishing before one, testing a webhook that isn't set | Follow `message` |\n| 413 | `payload_too_large` | Body too large | Send less |\n| 415 | `unsupported_media_type` | Body without `Content-Type: application/json` | Set the header |\n| 429 | `rate_limited` | Too many requests | Wait `Retry-After` seconds |\n| 500 | `internal_error` | Our fault (details are logged on our side) | Retry; contact support if it persists |\n| 503 | `suppliers_unavailable` | Every number supplier failed or timed out (`details.suppliersTried`). Nothing is billed | Retry after `Retry-After` (30 s) |\n\n## Rate limits\n\nEach account may make 5 requests per second with bursts of 20 (your account manager can raise this). The limit is soft: a request that would only wait up to 1 s for its turn is held and then served, so a client going slightly too fast is slowed down rather than failed. Beyond that you get `429 rate_limited` with `Retry-After`. Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Requests with an unknown key are limited per IP address.\n\nFor receiving SMS, a webhook (or one long-poll per order with `Prefer: wait=10`) uses far fewer requests than polling in a loop.\n\n## Reliability\n\nNumbers come from several suppliers. If one fails, the order is retried with the next before you get an answer, so a supplier outage usually doesn't reach you. `503 suppliers_unavailable` means every supplier failed: retry after `Retry-After`. Nothing is billed for an order that failed.\n\n## Conventions\n\n- **Ids** (`id`, `orderId`, SMS `id`) are 64-bit integers sent as JSON numbers. They can exceed 2^53, so parse them as 64-bit integers or BigInt, not as doubles.\n- **Phone numbers** are strings in international format without `+`, e.g. `\"84912340000\"`.\n- **Countries** are lower-case English names (`vietnam`, `united kingdom`) and **services** are short codes (`wa`, `tg`); both come from `GET /prices`. In a path, encode spaces as `%20`: `/prices/united%20kingdom`.\n- **Prices** are USD with up to 4 decimals, already including your discount.\n- **Times** are RFC 3339, in UTC.\n\n## No query strings\n\nIdentifiers go in the path, filters go in a JSON body (`POST /orders/search`), and the long-poll is a `Prefer` header. The older query forms (`?country=`, `?service=`, filters on `GET /orders`, `?wait=`) still work but are deprecated: their responses carry `Deprecation` and a `Link` header with the replacement."
  },
  "servers": [
    {
      "url": "/",
      "description": "This server"
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Your account: discount and usage totals."
    },
    {
      "name": "Catalogue",
      "description": "What you can buy, where, and at what price. Prices already include your discount, and `available` is the number of phone numbers in stock at that price right now (it changes every few seconds). Start here to find the `country` and `service` values to order with."
    },
    {
      "name": "Orders",
      "description": "Buy numbers and read their SMS. Lifecycle: `waiting` → `received` → `completed`; `cancelled` (before any SMS) and `expired` (no SMS in time) are not billed. See **Order lifecycle and billing** above."
    },
    {
      "name": "Webhook",
      "description": "Have every SMS pushed to your server as it arrives, signed per [Standard Webhooks](https://www.standardwebhooks.com), instead of polling for it.\n\n**Where SMS are sent:** to the webhook URL **you enter** — with `PUT /api/v2/client/webhook`, or set by your account manager in the dashboard. One URL per account; it receives every SMS on your orders. With no URL set nothing is pushed: read the SMS from the order instead."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/api/v2/client/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getAccount",
        "summary": "Your account, discount and order totals",
        "responses": {
          "200": {
            "description": "Your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                },
                "example": {
                  "id": 7,
                  "name": "Acme",
                  "email": "ops@acme.test",
                  "currency": "USD",
                  "discountPct": 5,
                  "stats": {
                    "orders": 120,
                    "active": 2,
                    "completed": 101,
                    "cancelled": 12,
                    "expired": 5,
                    "spent": 68.4
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Who the API key belongs to, the discount applied to every price you see, and totals over all your orders: how many are open (`active` = waiting or received), completed, cancelled and expired, and `spent`, the sum of your billed orders in USD."
      }
    },
    "/api/v2/client/countries": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listCountries",
        "summary": "Countries with something for sale",
        "responses": {
          "200": {
            "description": "Countries, A-Z.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CountrySummary"
                  }
                },
                "example": [
                  {
                    "country": "vietnam",
                    "dialCode": "84",
                    "services": 12,
                    "available": 3400
                  }
                ]
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "One entry per country where at least one service is for sale to you, with its dial code, how many services it has and how many numbers are in stock across them. Use it to build a country picker; then `GET /services/{country}` or `GET /prices/{country}` for what's in it."
      }
    },
    "/api/v2/client/services": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listServices",
        "summary": "Services for sale",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Only services sold in this country. Deprecated: use `GET /services/{country}`.",
            "schema": {
              "type": "string"
            },
            "example": "vietnam",
            "deprecated": true
          }
        ],
        "responses": {
          "200": {
            "description": "Services, A-Z, with the lowest price.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceSummary"
                  }
                },
                "example": [
                  {
                    "service": "wa",
                    "name": "WhatsApp",
                    "countries": 31,
                    "minPrice": 0.42,
                    "currency": "USD",
                    "available": 9800
                  }
                ]
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "One entry per service (WhatsApp, Telegram, …) for sale to you anywhere: in how many countries, its lowest price among them (`minPrice`, after your discount) and the total stock. For one country only, use `GET /services/{country}`."
      }
    },
    "/api/v2/client/services/{country}": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listServicesInCountry",
        "summary": "Services for sale in one country",
        "parameters": [
          {
            "name": "country",
            "in": "path",
            "required": true,
            "description": "Country name as in `GET /prices`, lower case (spaces as `%20`).",
            "schema": {
              "type": "string"
            },
            "example": "vietnam"
          }
        ],
        "responses": {
          "200": {
            "description": "Services, A-Z, with the lowest price.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceSummary"
                  }
                },
                "example": [
                  {
                    "service": "wa",
                    "name": "WhatsApp",
                    "countries": 31,
                    "minPrice": 0.42,
                    "currency": "USD",
                    "available": 9800
                  }
                ]
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Like `GET /services`, limited to one country: `countries` is 1 and `minPrice` is that country's price. An unknown country, or one with nothing for sale, gives an empty list."
      }
    },
    "/api/v2/client/prices": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listPrices",
        "summary": "Your price and stock for every country and service",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Filter by country. Deprecated: use `GET /prices/{country}` or `GET /prices/{country}/{service}`.",
            "schema": {
              "type": "string"
            },
            "example": "vietnam",
            "deprecated": true
          },
          {
            "name": "service",
            "in": "query",
            "required": false,
            "description": "Filter by service code. Deprecated: use `GET /prices/{country}` or `GET /prices/{country}/{service}`.",
            "schema": {
              "type": "string"
            },
            "example": "wa",
            "deprecated": true
          }
        ],
        "responses": {
          "200": {
            "description": "One row per country + service you can buy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Price"
                  }
                },
                "example": [
                  {
                    "country": "vietnam",
                    "dialCode": "84",
                    "service": "wa",
                    "serviceName": "WhatsApp",
                    "price": 0.684,
                    "currency": "USD",
                    "available": 240
                  }
                ]
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "The full price list: one row per country + service you can buy, with your price (discount included) and the numbers in stock at that price. This is what `POST /orders` charges if you buy now. Stock changes every few seconds; a row with `available: 0` can still be ordered, but will likely answer `409 no_numbers`."
      }
    },
    "/api/v2/client/prices/{country}": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "listPricesInCountry",
        "summary": "Your price and stock in one country",
        "parameters": [
          {
            "name": "country",
            "in": "path",
            "required": true,
            "description": "Country name as in `GET /prices`, lower case (spaces as `%20`).",
            "schema": {
              "type": "string"
            },
            "example": "vietnam"
          }
        ],
        "responses": {
          "200": {
            "description": "One row per country + service you can buy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Price"
                  }
                },
                "example": [
                  {
                    "country": "vietnam",
                    "dialCode": "84",
                    "service": "wa",
                    "serviceName": "WhatsApp",
                    "price": 0.684,
                    "currency": "USD",
                    "available": 240
                  }
                ]
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "The rows of `GET /prices` for one country. An unknown country, or one with nothing for sale, gives an empty list."
      }
    },
    "/api/v2/client/prices/{country}/{service}": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "operationId": "getPriceForRoute",
        "summary": "Your price and stock for one country + service",
        "parameters": [
          {
            "name": "country",
            "in": "path",
            "required": true,
            "description": "Country name as in `GET /prices`, lower case (spaces as `%20`).",
            "schema": {
              "type": "string"
            },
            "example": "vietnam"
          },
          {
            "name": "service",
            "in": "path",
            "required": true,
            "description": "Service code as in `GET /prices`, e.g. `wa` (WhatsApp), `tg` (Telegram).",
            "schema": {
              "type": "string"
            },
            "example": "wa"
          }
        ],
        "responses": {
          "200": {
            "description": "One row per country + service you can buy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Price"
                  }
                },
                "example": [
                  {
                    "country": "vietnam",
                    "dialCode": "84",
                    "service": "wa",
                    "serviceName": "WhatsApp",
                    "price": 0.684,
                    "currency": "USD",
                    "available": 240
                  }
                ]
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "The price you'd pay to buy this service in this country right now, and the stock at that price. A list with one row, or an empty list if this country + service isn't for sale. Check it before ordering if you want to show the price first; or send `maxPrice` with the order and let it refuse a price that's too high."
      }
    },
    "/api/v2/client/orders": {
      "post": {
        "tags": [
          "Orders"
        ],
        "operationId": "createOrder",
        "summary": "Buy a number",
        "description": "Buys a phone number for `country` + `service` and returns the order, `waiting` for its SMS. The request waits for a supplier to issue the number (usually well under a second; at most about 10 s).\n\n- **Price**: you pay your current price for the route (see `GET /prices`), fixed for this order. Send `maxPrice` to refuse anything higher (`409 price_above_max`, nothing bought).\n- **Billing**: only if an SMS arrives. Cancel unused orders to release the number at once; otherwise they expire unbilled at `expiresAt`.\n- **Retries**: send an `Idempotency-Key` header (or `idempotencyKey` in the body). A repeat with the same key returns the first order with `200` and buys nothing.\n- **Excluding numbers**: `excludePrefixes` (digits, country code included, e.g. `\"8490\"`) guarantees no number starting with them is issued.\n- **No stock**: `409 no_numbers`; every supplier down: `503 suppliers_unavailable`. Neither is billed.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique per purchase (up to 128 characters, e.g. a UUID). A repeat with the same key returns the first order (200) and buys nothing. Same as `idempotencyKey` in the body.",
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "example": "7d1f2c9e-order-1"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewOrder"
              },
              "example": {
                "country": "vietnam",
                "service": "wa",
                "maxPrice": 0.8
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Number issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": 8816265315333972,
                  "status": "waiting",
                  "country": "vietnam",
                  "service": "wa",
                  "serviceName": "WhatsApp",
                  "phone": "84912340000",
                  "price": 0.684,
                  "currency": "USD",
                  "billed": true,
                  "createdAt": "2026-11-02T07:51:40Z",
                  "expiresAt": "2026-11-02T08:11:40Z",
                  "finishedAt": null,
                  "smsCount": 0,
                  "code": null,
                  "sms": []
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "200": {
            "description": "Same `Idempotency-Key` as an earlier order: that order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "400": {
            "description": "Invalid body or parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "maxPrice must be > 0"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not sold for this country and service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "unknown_route",
                    "message": "unknown country or service, or not for sale"
                  }
                }
              }
            }
          },
          "409": {
            "description": "Not possible right now. `code` is one of `no_numbers` (`details.suppliersTried`), `price_above_max` (`details.price`), `not_priced`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "no_numbers": {
                    "value": {
                      "error": {
                        "code": "no_numbers",
                        "message": "no numbers available right now",
                        "details": {
                          "suppliersTried": 2
                        }
                      }
                    }
                  },
                  "price_above_max": {
                    "value": {
                      "error": {
                        "code": "price_above_max",
                        "message": "price 0.684 is above maxPrice",
                        "details": {
                          "price": 0.684
                        }
                      }
                    }
                  },
                  "not_priced": {
                    "value": {
                      "error": {
                        "code": "not_priced",
                        "message": "no price is available for this country and service right now"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Every supplier failed or timed out. Retry after `Retry-After` seconds. Not billed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "suppliers_unavailable",
                    "message": "number suppliers are temporarily unavailable; retry shortly",
                    "details": {
                      "suppliersTried": 2
                    }
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "tags": [
          "Orders"
        ],
        "operationId": "listOrders",
        "summary": "Latest orders",
        "description": "Your most recent orders, newest first, 50 per page, with `total` = all your orders. Orders in a list don't include SMS texts (only `smsCount` and `code`); get one order for those. To filter or page, use `POST /orders/search`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "`waiting`, `received`, `completed`, `cancelled`, `expired`, or `active` (waiting or received). Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "string",
              "enum": [
                "waiting",
                "received",
                "completed",
                "cancelled",
                "expired",
                "active"
              ]
            },
            "deprecated": true
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Filter by country. Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "string"
            },
            "deprecated": true
          },
          {
            "name": "service",
            "in": "query",
            "required": false,
            "description": "Filter by service code. Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "string"
            },
            "deprecated": true
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Created on or after: `YYYY-MM-DD` (UTC) or RFC 3339. Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-01",
            "deprecated": true
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Created on or before (whole day for `YYYY-MM-DD`). Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-11-02",
            "deprecated": true
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Order id or phone digits. Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "string"
            },
            "deprecated": true
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, from 1. Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "deprecated": true
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Orders per page. Deprecated: send it in the body of `POST /orders/search`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "deprecated": true
          }
        ],
        "responses": {
          "200": {
            "description": "A page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderPage"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            }
          },
          "400": {
            "description": "Bad date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "from/to must be YYYY-MM-DD or RFC 3339"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v2/client/orders/search": {
      "post": {
        "tags": [
          "Orders"
        ],
        "operationId": "searchOrders",
        "summary": "Search your order history",
        "description": "Your orders, newest first, filtered by the JSON body. Every field is optional and they combine (AND); an empty body or `{}` gives the latest orders like `GET /orders`.\n\n- `status`: one status, or `active` for `waiting` + `received`.\n- `country`, `service`: exact values, as in `GET /prices`.\n- `from`, `to`: creation time. `YYYY-MM-DD` means the whole UTC day (inclusive); RFC 3339 is exact.\n- `q`: an order id, or digits of the phone number.\n- `page` (from 1) and `limit` (1-200, default 50); `total` counts every match.\n\nUnknown fields are a `400`, so a misspelt filter is never silently ignored.",
        "responses": {
          "200": {
            "description": "A page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderPage"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "400": {
            "description": "Bad date, or an unknown or malformed field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "from/to must be YYYY-MM-DD or RFC 3339"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "waiting",
                      "received",
                      "completed",
                      "cancelled",
                      "expired",
                      "active"
                    ],
                    "description": "`waiting`, `received`, `completed`, `cancelled`, `expired`, or `active` (waiting or received)."
                  },
                  "country": {
                    "type": "string",
                    "description": "Filter by country."
                  },
                  "service": {
                    "type": "string",
                    "description": "Filter by service code."
                  },
                  "from": {
                    "type": "string",
                    "description": "Created on or after: `YYYY-MM-DD` (UTC) or RFC 3339.",
                    "examples": [
                      "2026-11-01"
                    ]
                  },
                  "to": {
                    "type": "string",
                    "description": "Created on or before (whole day for `YYYY-MM-DD`).",
                    "examples": [
                      "2026-11-02"
                    ]
                  },
                  "q": {
                    "type": "string",
                    "description": "Order id or phone digits."
                  },
                  "page": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1,
                    "description": "Page number, from 1."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 50,
                    "description": "Orders per page."
                  }
                }
              },
              "example": {
                "status": "completed",
                "country": "vietnam",
                "from": "2026-11-01",
                "to": "2026-11-02",
                "page": 1,
                "limit": 50
              }
            }
          }
        }
      }
    },
    "/api/v2/client/orders/{id}": {
      "get": {
        "tags": [
          "Orders"
        ],
        "operationId": "getOrder",
        "summary": "One order, with its SMS and code",
        "description": "The order with every SMS received on it (oldest first) and `code`, the verification code from the latest one.\n\n**Long-poll**: send `Prefer: wait=N` (N up to 10). While the order is `waiting`, the request is held until an SMS arrives or the order closes, or N seconds pass, then returns the order as it is; the response carries `Preference-Applied: wait=N`. Repeat while `status` is `waiting`. This is the cheapest way to wait for an SMS without a webhook.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Order id.",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8816265315333972
          },
          {
            "name": "Prefer",
            "in": "header",
            "required": false,
            "description": "`wait=N` (RFC 7240): seconds to wait for an SMS while the order is `waiting` (0-10). Returns as soon as one arrives or the order closes.",
            "schema": {
              "type": "string"
            },
            "example": "wait=10"
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "description": "Seconds to wait for an SMS while the order is `waiting` (0-10). Deprecated: send `Prefer: wait=N` instead.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10,
              "default": 0
            },
            "example": 10,
            "deprecated": true
          }
        ],
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                },
                "example": {
                  "id": 8816265315333972,
                  "status": "received",
                  "country": "vietnam",
                  "service": "wa",
                  "serviceName": "WhatsApp",
                  "phone": "84912340000",
                  "price": 0.684,
                  "currency": "USD",
                  "billed": true,
                  "createdAt": "2026-11-02T07:51:40Z",
                  "expiresAt": "2026-11-02T08:11:40Z",
                  "finishedAt": null,
                  "smsCount": 1,
                  "code": "482913",
                  "sms": [
                    {
                      "id": 42,
                      "from": "WhatsApp",
                      "text": "Your WhatsApp code 482-913",
                      "code": "482913",
                      "receivedAt": "2026-11-02T07:52:21Z"
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Preference-Applied": {
                "description": "`wait=N` when a wait was applied.",
                "schema": {
                  "type": "string"
                },
                "example": "wait=10"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/OrderNotFound"
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v2/client/orders/{id}/finish": {
      "post": {
        "tags": [
          "Orders"
        ],
        "operationId": "finishOrder",
        "summary": "Finish: done with the number",
        "description": "Closes the order as `completed` once you have the code. Only allowed after an SMS arrived (`409 invalid_state` before: cancel instead, which isn't billed). Billed at the order's price. Finishing a completed order again returns it unchanged; finishing a cancelled or expired one is `409 invalid_state`. If you never finish an order that received an SMS, it completes on its own at `expiresAt`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Order id.",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8816265315333972
          }
        ],
        "responses": {
          "200": {
            "description": "The finished order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/OrderNotFound"
          },
          "409": {
            "description": "Not possible in this state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_state",
                    "message": "no SMS received yet; cancel the order instead (it won't be billed)"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v2/client/orders/{id}/cancel": {
      "post": {
        "tags": [
          "Orders"
        ],
        "operationId": "cancelOrder",
        "summary": "Cancel: no SMS, not billed",
        "description": "Closes an order that hasn't received an SMS as `cancelled`, releases the number and bills nothing. Do this as soon as you know the SMS isn't coming (the service rejected the number, or you no longer need it). After an SMS has arrived it's `409 invalid_state` (finish it instead). Cancelling a cancelled order again returns it unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Order id.",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 8816265315333972
          }
        ],
        "responses": {
          "200": {
            "description": "The cancelled order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/OrderNotFound"
          },
          "409": {
            "description": "An SMS already arrived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_state",
                    "message": "an SMS was received; finish the order instead"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v2/client/webhook": {
      "get": {
        "tags": [
          "Webhook"
        ],
        "operationId": "getWebhook",
        "summary": "Your webhook URL and signing secret",
        "responses": {
          "200": {
            "description": "Webhook settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Where SMS are pushed (`url`, `null` when none is set) and the `whsec_...` secret that signs every push. Pushes are only sent while a URL is set."
      },
      "put": {
        "tags": [
          "Webhook"
        ],
        "operationId": "setWebhook",
        "summary": "Set the webhook URL",
        "description": "Enter the URL where your SMS should be sent. From now on every SMS on your orders is POSTed to this URL as an `sms.received` event (see the webhook below). The URL may be `http://` or `https://` (recommended), must resolve to a public address (checked before every push), and is not followed through redirects. Replaces any previous URL; the signing secret stays the same.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              },
              "example": {
                "url": "https://example.com/hooks/sms"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "400": {
            "description": "Bad URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "the webhook URL must start with http:// or https://"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhook"
        ],
        "operationId": "deleteWebhook",
        "summary": "Stop pushing SMS",
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Removes the webhook URL. SMS keep arriving on your orders and stay readable with `GET /orders/{id}`; pushes still being retried are dropped."
      }
    },
    "/api/v2/client/webhook/secret": {
      "post": {
        "tags": [
          "Webhook"
        ],
        "operationId": "rotateWebhookSecret",
        "summary": "New signing secret",
        "description": "Replaces the signing secret and returns the new one. Every push from now on, including retries of earlier ones, is signed with the new secret, so have your verifier accept both the old and the new secret until it is deployed with the new one.",
        "responses": {
          "200": {
            "description": "Settings with the new secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v2/client/webhook/test": {
      "post": {
        "tags": [
          "Webhook"
        ],
        "operationId": "testWebhook",
        "summary": "Send a test event now",
        "responses": {
          "200": {
            "description": "How the delivery went.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "delivered",
                    "webhookId"
                  ],
                  "properties": {
                    "delivered": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Why it failed."
                    },
                    "webhookId": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "delivered": true,
                  "error": null,
                  "webhookId": "msg_test_1793606400000"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            }
          },
          "409": {
            "description": "No webhook URL set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "invalid_state",
                    "message": "no webhook URL is set"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidKey"
          },
          "403": {
            "$ref": "#/components/responses/AccountDisabled"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "POSTs a signed `webhook.test` event (`{\"type\": \"webhook.test\", \"timestamp\": ..., \"data\": {\"customerId\": ...}}`) to your URL right away and reports whether your server accepted it (`delivered`, and `error` when it didn't). Use it to check your signature verification. Not retried. `409 invalid_state` when no URL is set."
      }
    }
  },
  "webhooks": {
    "sms.received": {
      "post": {
        "tags": [
          "Webhook"
        ],
        "operationId": "smsReceived",
        "summary": "An SMS arrived on one of your orders",
        "description": "POSTed to **the webhook URL you entered** (`PUT /api/v2/client/webhook`, or set by your account manager) for every SMS on your orders, as it arrives.\n\n1. **Verify** `webhook-signature` against the raw body before parsing it (any Standard Webhooks library does this), and check `webhook-timestamp` is recent.\n2. **Deduplicate** on `webhook-id`: a push can arrive more than once.\n3. **Answer 2xx quickly** (within 10 s); do slow work afterwards.\n\nNetwork errors, timeouts, `408`, `429` and `5xx` are retried after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h, then the push gives up; any other answer stops it at once. Either way the SMS stays readable with `GET /orders/{id}`.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "msg_sms_42",
            "description": "`msg_sms_<SMS id>`. Identical on every retry of the same SMS: deduplicate on it."
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "example": 1793606400,
            "description": "Unix time the push was signed. Reject pushes more than a few minutes old to block replays."
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=",
            "description": "`v1,` + base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with your secret (base64-decoded after `whsec_`)."
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsReceivedEvent"
              },
              "example": {
                "type": "sms.received",
                "timestamp": "2026-11-02T07:52:22Z",
                "data": {
                  "orderId": 8816265315333972,
                  "phone": "84912340000",
                  "country": "vietnam",
                  "service": "wa",
                  "sms": {
                    "id": 42,
                    "from": "WhatsApp",
                    "text": "Your WhatsApp code 482-913",
                    "code": "482913",
                    "receivedAt": "2026-11-02T07:52:21Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx acknowledges the event."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Customer API key (`sk_...`). `X-API-Key: sk_...` also works."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Burst size.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current burst.",
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      },
      "Deprecation": {
        "description": "Present when a deprecated query-string form was used (RFC 9745): when it was deprecated, as `@` + Unix time.",
        "schema": {
          "type": "string"
        },
        "example": "@1793664000"
      },
      "Link": {
        "description": "With `Deprecation`: the replacement, e.g. `</api/v2/client/prices/vietnam>; rel=\"successor-version\"`.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "InvalidKey": {
        "description": "Missing or unknown API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_key",
                "message": "invalid API key"
              }
            }
          }
        }
      },
      "AccountDisabled": {
        "description": "Your account is disabled.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "account_disabled",
                "message": "account disabled"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "too many requests; retry in 2 s"
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        }
      },
      "InternalError": {
        "description": "Server error (logged on our side).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "internal error"
              }
            }
          }
        }
      },
      "OrderNotFound": {
        "description": "No such order on your account.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "order not found"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable.",
                "examples": [
                  "no_numbers",
                  "invalid_key",
                  "rate_limited",
                  "not_found"
                ]
              },
              "message": {
                "type": "string",
                "description": "For people; may change."
              },
              "details": {
                "type": "object",
                "description": "Extra data for some codes.",
                "additionalProperties": true
              }
            },
            "description": "What went wrong. Branch on `code`."
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "id",
          "name",
          "email",
          "currency",
          "discountPct",
          "stats"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Your account id."
          },
          "name": {
            "type": "string",
            "description": "Account name."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contact email, if set."
          },
          "currency": {
            "type": "string",
            "const": "USD",
            "description": "Currency of every price and total: always USD."
          },
          "discountPct": {
            "type": "number",
            "description": "Your discount on list prices, in percent. Every price you see already includes it."
          },
          "stats": {
            "type": "object",
            "properties": {
              "orders": {
                "type": "integer",
                "description": "Orders placed."
              },
              "active": {
                "type": "integer",
                "description": "Open orders: waiting or received."
              },
              "completed": {
                "type": "integer",
                "description": "Completed orders."
              },
              "cancelled": {
                "type": "integer",
                "description": "Cancelled orders (not billed)."
              },
              "expired": {
                "type": "integer",
                "description": "Orders that expired without an SMS (not billed)."
              },
              "spent": {
                "type": "number",
                "description": "Sum of your billed orders, USD."
              }
            },
            "description": "Totals over all your orders."
          }
        }
      },
      "CountrySummary": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string",
            "description": "Country name, to use in paths and orders."
          },
          "dialCode": {
            "type": "string",
            "description": "International dialling code, without `+`."
          },
          "services": {
            "type": "integer",
            "description": "Services for sale in this country."
          },
          "available": {
            "type": "integer",
            "description": "Numbers in stock across services."
          }
        }
      },
      "ServiceSummary": {
        "type": "object",
        "properties": {
          "service": {
            "type": "string",
            "description": "Service code, to use in paths and orders."
          },
          "name": {
            "type": "string",
            "description": "Service name, for display."
          },
          "countries": {
            "type": "integer",
            "description": "Countries where it is for sale."
          },
          "minPrice": {
            "type": "number",
            "description": "Lowest price among those countries, USD, after your discount."
          },
          "currency": {
            "type": "string",
            "description": "Always USD."
          },
          "available": {
            "type": "integer",
            "description": "Numbers in stock across those countries."
          }
        }
      },
      "Price": {
        "type": "object",
        "properties": {
          "country": {
            "type": "string",
            "description": "Country name, to use in orders."
          },
          "dialCode": {
            "type": "string",
            "description": "International dialling code, without `+`. Issued numbers start with it."
          },
          "service": {
            "type": "string",
            "description": "Service code, to use in orders."
          },
          "serviceName": {
            "type": "string",
            "description": "Service name, for display."
          },
          "price": {
            "type": "number",
            "description": "What `POST /orders` charges for this route right now, USD, after your discount."
          },
          "currency": {
            "type": "string",
            "description": "Always USD."
          },
          "available": {
            "type": "integer",
            "description": "Numbers in stock at this price right now. Changes every few seconds."
          }
        }
      },
      "NewOrder": {
        "type": "object",
        "required": [
          "country",
          "service"
        ],
        "properties": {
          "country": {
            "type": "string",
            "description": "Country name as in `GET /prices`.",
            "examples": [
              "vietnam"
            ]
          },
          "service": {
            "type": "string",
            "description": "Service code as in `GET /prices`.",
            "examples": [
              "wa"
            ]
          },
          "operator": {
            "type": "string",
            "description": "Mobile operator, when the supplier lets you choose one. Default `any`."
          },
          "maxPrice": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "The most you'll pay, USD. If the current price is higher the order is refused with `409 price_above_max` (and `details.price`) and nothing is bought."
          },
          "excludePrefixes": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "string"
            },
            "description": "Never issue a number starting with any of these digit strings, country code included (e.g. `\"8490\"`). Up to 50."
          },
          "idempotencyKey": {
            "type": "string",
            "maxLength": 128,
            "description": "Same as the Idempotency-Key header."
          }
        }
      },
      "OrderStatus": {
        "type": "string",
        "enum": [
          "waiting",
          "received",
          "completed",
          "cancelled",
          "expired"
        ],
        "description": "`waiting`: no SMS yet. `received`: an SMS arrived. `completed`: finished (billed). `cancelled`: cancelled before any SMS (not billed). `expired`: no SMS before `expiresAt` (not billed)."
      },
      "Sms": {
        "type": "object",
        "required": [
          "id",
          "text",
          "receivedAt"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "SMS id (64-bit integer). The webhook's `webhook-id` is `msg_sms_<id>`."
          },
          "from": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sender as reported by the network (e.g. `WhatsApp`), or `null`."
          },
          "text": {
            "type": "string",
            "description": "Full SMS text."
          },
          "code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Verification code found in the text: the first group of 4-8 digits, or `123-456` / `123 456` joined. `null` when none."
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When it arrived."
          }
        }
      },
      "Order": {
        "type": "object",
        "required": [
          "id",
          "status",
          "country",
          "service",
          "phone",
          "price",
          "currency",
          "billed",
          "createdAt",
          "expiresAt",
          "smsCount"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "Order id. A 64-bit integer: parse it as one (it can exceed 2^53)."
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus",
            "description": "Where the order is in its lifecycle."
          },
          "country": {
            "type": "string",
            "description": "Country name."
          },
          "service": {
            "type": "string",
            "description": "Service code."
          },
          "serviceName": {
            "type": "string",
            "description": "Service name, for display."
          },
          "phone": {
            "type": "string",
            "description": "The issued number, in international format without `+`. Use it on the service to receive the SMS."
          },
          "price": {
            "type": "number",
            "description": "What this order costs if billed, USD. Fixed when bought."
          },
          "currency": {
            "type": "string",
            "description": "Always USD."
          },
          "billed": {
            "type": "boolean",
            "description": "Whether it counts towards what you pay: `true` unless cancelled, or expired without an SMS."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the number was bought."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When an order still waiting expires (unbilled), or one with an SMS completes on its own."
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was finished, cancelled or expired; `null` while open."
          },
          "smsCount": {
            "type": "integer",
            "description": "SMS received so far."
          },
          "code": {
            "type": "string",
            "description": "Verification code from the latest SMS; omitted or `null` when none was found."
          },
          "sms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Sms"
            },
            "description": "Every SMS received, oldest first. Only on single-order responses (not in lists)."
          }
        }
      },
      "OrderPage": {
        "type": "object",
        "required": [
          "orders",
          "total",
          "page",
          "limit"
        ],
        "properties": {
          "orders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Order"
            },
            "description": "This page of orders, newest first, without SMS texts."
          },
          "total": {
            "type": "integer",
            "description": "Orders matching the filters, across all pages."
          },
          "page": {
            "type": "integer",
            "description": "This page number, from 1."
          },
          "limit": {
            "type": "integer",
            "description": "Orders per page."
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where SMS are pushed; `null` when none is set."
          },
          "secret": {
            "type": "string",
            "description": "`whsec_...` signing secret: base64-decode what follows `whsec_` to get the HMAC key."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Event types pushed: `sms.received`."
          },
          "signing": {
            "type": "string",
            "description": "The signature scheme."
          }
        },
        "example": {
          "url": "https://example.com/hooks/sms",
          "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw",
          "events": [
            "sms.received"
          ],
          "signing": "Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature: v1,<HMAC-SHA256>)"
        }
      },
      "SmsReceivedEvent": {
        "type": "object",
        "required": [
          "type",
          "timestamp",
          "data"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "sms.received",
            "description": "Event type: `sms.received`."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was created (the SMS's arrival, give or take a moment)."
          },
          "data": {
            "type": "object",
            "properties": {
              "orderId": {
                "type": "integer",
                "format": "int64",
                "description": "The order the SMS arrived on."
              },
              "phone": {
                "type": "string",
                "description": "The order's phone number."
              },
              "country": {
                "type": "string",
                "description": "The order's country."
              },
              "service": {
                "type": "string",
                "description": "The order's service code."
              },
              "sms": {
                "$ref": "#/components/schemas/Sms",
                "description": "The SMS, with its extracted `code`."
              }
            },
            "description": "The order and the SMS."
          }
        }
      }
    }
  }
}
