# RelySMS — 短信接收 API — 接入你自己的软件

如果你的软件已经通过 handler_api.php 对接过接码服务，它就能直接用于 RelySMS：只需更换服务器地址和密钥。同样的操作、同样的响应，按我们的价格和退款规则。

- 地址: `https://relysms.pro/stubs/handler_api.php`
- Auth: `api_key` parameter
- Format: plain text tokens, JSON where noted

## 快速开始

1. 登录账户，打开“余额”→“API”→“创建密钥”。密钥只显示一次，请妥善保存。
2. 将余额充值到 $20 或以上：只有在此余额下才能通过 API 购买。余额为零也可以持有密钥。
3. 在软件中把服务器地址设为 https://relysms.pro/stubs/handler_api.php，并用你的密钥替换原服务的密钥。
4. 购买号码（getNumber），每 5–10 秒查询一次状态（getStatus），直到返回带验证码的 STATUS_OK。
5. 完成订单（setStatus=6）。如果没有收到验证码，2 分钟后取消（setStatus=8），或等待时间窗口结束：钱会自动退回。

## 规则与限制

- 通过 API 购买需要余额不少于 $20。低于此值时 getNumber 返回 NO_BALANCE，其他方法照常可用。
- 号码归你使用 20 分钟。没有验证码——订单自动关闭，钱退回余额。
- 购买 2 分钟后，如果还没有验证码，可以取消订单，钱立即退回。
- 收到第一条验证码后订单即视为已付费，不再退款。时间窗口内，后续验证码仍可能到达。
- 每个密钥每秒最多 20 次请求、5 次购买；同时最多 50 个未完成订单。
- 价格在购买时锁定。maxPrice 可防止以高于你设定的价格购买。
- 适用服务条款：禁止垃圾信息、欺诈及以此为目的的批量注册。

## 方法

所有请求都以 GET 或 POST 发往同一地址，参数放在查询字符串或表单正文中。密钥放在 api_key 参数，操作放在 action。

### `getBalance`

账户余额（美元）。

仅需 api_key 和 action。

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getBalance
```

响应:

```
ACCESS_BALANCE:25.00
```

### `getNumber`

购买某国家某服务的号码。立即扣款；如果没有收到验证码则退回。响应：ACCESS_NUMBER:订单号:电话。

| 参数 | 必填 | 说明 |
|---|---|---|
| `service` | 是 | 服务代码（见 getServicesList），例如 tg。 |
| `country` | 是 | 国家代码（见 getCountries）。 |
| `maxPrice` | 否 | 最高价格（美元）。超过时返回 WRONG_MAX_PRICE 及当前价格，不扣款。 |

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getNumber&service=tg&country=187&maxPrice=0.80
```

响应:

```
ACCESS_NUMBER:1042:15550001111
```

可能的错误: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`, `BANNED`

### `getNumberV2`

与 getNumber 相同，但以 JSON 返回价格和时间窗口。

| 参数 | 必填 | 说明 |
|---|---|---|
| `service` | 是 | 服务代码。 |
| `country` | 是 | 国家代码。 |
| `maxPrice` | 否 | 最高价格（美元）。 |

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getNumberV2&service=tg&country=187&maxPrice=0.80
```

响应:

```
{
  "activationId": 1042,
  "phoneNumber": "15550001111",
  "activationCost": 0.59,
  "countryCode": "187",
  "canGetAnotherSms": true,
  "activationTime": "2026-10-27T10:00:00.000Z",
  "activationEndTime": "2026-10-27T10:20:00.000Z"
}
```

可能的错误: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`

### `getStatus`

订单状态：等待验证码、已收到（含验证码）或已取消。

| 参数 | 必填 | 说明 |
|---|---|---|
| `id` | 是 | getNumber 返回的订单号。 |

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getStatus&id=1042
```

响应:

```
STATUS_OK:48213
```

可能的错误: `NO_ACTIVATION`

### `setStatus`

操作订单：就绪、再要一个码、完成、取消（见“状态”）。

| 参数 | 必填 | 说明 |
|---|---|---|
| `id` | 是 | 订单号。 |
| `status` | 是 | 1、3、6 或 8。 |

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=setStatus&id=1042&status=6
```

响应:

```
ACCESS_ACTIVATION
```

可能的错误: `NO_ACTIVATION`, `BAD_STATUS`, `EARLY_CANCEL_DENIED`, `ERROR_SQL`

### `getActiveActivations`

该密钥的所有未完成订单：号码、状态、已收到的验证码和正文。

仅需 api_key 和 action。

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getActiveActivations
```

响应:

```
{
  "status": "success",
  "activeActivations": [
    {
      "activationId": 1042,
      "serviceCode": "tg",
      "phoneNumber": "15550001111",
      "activationCost": 0.59,
      "activationStatus": "2",
      "smsCode": ["48213"],
      "smsText": ["Telegram code: 48213"],
      "activationTime": "2026-10-27T10:00:00.000Z",
      "countryCode": "187"
    }
  ]
}
```

### `getPrices`

我们的价格（美元）和大致号码数量。不带参数时返回所有在售项。

| 参数 | 必填 | 说明 |
|---|---|---|
| `service` | 否 | 仅此服务。 |
| `country` | 否 | 仅此国家。 |

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getPrices&service=tg&country=187
```

响应:

```
{ "187": { "tg": { "cost": 0.59, "count": 1000 } } }
```

可能的错误: `BAD_SERVICE`, `BAD_COUNTRY`

### `getCountries`

带协议代码及俄文、英文名称的国家列表。

仅需 api_key 和 action。

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getCountries
```

响应:

```
{ "187": { "id": 187, "rus": "США", "eng": "United States", "visible": 1 } }
```

### `getServicesList`

带协议代码的服务列表。

仅需 api_key 和 action。

```
GET https://relysms.pro/stubs/handler_api.php?api_key=KEY&action=getServicesList
```

响应:

```
{ "status": "success", "services": [{ "code": "tg", "name": "Telegram" }] }
```

## 状态

setStatus 中 status 的取值：

- `status=1` → `ACCESS_READY` — 号码已填入服务，等待验证码。不做任何更改，仅为兼容保留。
- `status=3` → `ACCESS_RETRY_GET` — 在同一号码上再请求一个验证码——收到第一个验证码后、订单未关闭时可用。免费。新验证码到达前，getStatus 返回 STATUS_WAIT_RETRY。
- `status=6` → `ACCESS_ACTIVATION` — 收到验证码后完成订单。号码不会再收到新短信。
- `status=8` → `ACCESS_CANCEL` — 取消未收到验证码的订单（至少 2 分钟后）。钱退回余额。

getStatus 的响应：

- `STATUS_WAIT_CODE` — 还没有验证码——5–10 秒后再查询。
- `STATUS_OK:48213` — 验证码已到；冒号后是验证码本身（如果短信中没有码，则为短信正文）。
- `STATUS_WAIT_RETRY:48213` — 已请求另一个验证码，尚未到达；冒号后为上一个验证码。
- `STATUS_CANCEL` — 已取消或在无验证码的情况下关闭；钱已退回余额。

## 错误

错误以纯文本返回，这是该协议的惯例。除 RATE_LIMIT 外，均以状态码 200 返回。

- `NO_KEY` — 未传入 api_key。
- `BAD_KEY` — 密钥错误、已停用，或账户被封禁。
- `BAD_ACTION` — 未知操作。
- `BAD_SERVICE` — 没有这个服务代码。
- `BAD_COUNTRY` — 没有这个国家代码。
- `NO_NUMBERS` — 该组合暂时没有号码；未扣款。
- `NO_BALANCE` — 余额不足 $20（API 门槛）或低于号码价格。
- `WRONG_MAX_PRICE` — 价格高于 maxPrice；冒号后为当前价格。
- `LIMIT_ACTIVATIONS` — 未完成订单过多——请完成或取消一部分。
- `RATE_LIMIT` — 请求过于频繁；HTTP 429。请一秒后重试。
- `BANNED` — 账户已被封禁——请联系客服。
- `NO_ACTIVATION` — 该密钥下没有这个订单号。
- `BAD_STATUS` — 该操作当前不适用于此订单（例如在没有验证码时完成订单）。
- `EARLY_CANCEL_DENIED` — 购买 2 分钟后才能取消。
- `ERROR_SQL` — 我方临时错误——请一分钟后重试。

## 服务与国家代码

代码为该协议的标准代码：tg — Telegram，wa — WhatsApp 等；国家 187 — 美国。getServicesList 和 getCountries 返回完整列表——请从那里获取代码，而不是旧表格：服务和国家会随库存变化。

## REST API v1

与 handler_api.php 相同的号码和规则，但使用 JSON：服务用名称（telegram），国家用 ISO 代码（US），并提供便于程序分支处理的 HTTP 状态码和错误代码。新代码请用它；已经使用 handler_api.php 的软件保持不变即可。

- Base URL: `https://relysms.pro/api/v1`
- Auth: `Authorization: Bearer <key>` header
- Format: JSON

### 密钥

在请求头中发送密钥：Authorization: Bearer <密钥>（或 X-Api-Key: <密钥>）。不要放在 URL 中——URL 会进入日志。限制、$20 门槛和目录与 handler_api.php 相同。

### 重试而不重复购买

在 POST /orders 和 /orders/batch 中添加请求头 Idempotency-Key，值由你自定（UUID，最多 255 个字符）。如果连接中断而你用同一个键重发请求，将返回第一次的结果（带有 Idempotent-Replayed: true 头），不会重复购买。同一个键配不同的请求体会被拒绝；第一次仍在执行时到达的重复请求会得到 idempotency_in_progress。成功的结果保存 24 小时；失败的购买不会扣费，可用同一个键重试。

### 一次购买多个号码

POST /orders/batch 在一次请求中为同一服务和国家购买最多 20 个号码——这是每秒购买超过 5 个的方式。购买依次进行；第一个失败的购买会停止批次，响应中列出已买到的号码，原因在 stoppedBy 中。如果一个也没买到，响应就是该错误本身。

### 分页

GET /orders 先返回最新的订单。当 next 不为 null 时，把它作为 before 传入以获取下一页。

### Webhook

无需轮询 GET /orders/{id}：通过 PUT /webhook 设置一个 HTTPS 地址，当通过 API 购买的订单发生变化时，我们会向它发送 POST。请求体包含发送时刻的订单。请在 10 秒内返回任意 2xx；同一订单可能产生多个事件，重试后同一事件也可能到达两次，请以投递 id 去重。

- `order.code_received` — 收到验证码（每个新验证码都会再次发送）。
- `order.finished` — 订单已完成——由你完成，或收到验证码后时间到期。
- `order.cancelled` — 未收到验证码即取消；款项已退回余额。
- `order.expired` — 时间到且未收到验证码；款项已退回余额。
- `ping` — 来自 POST /webhook/test 的测试；order 为 null。

```http
POST /relysms-hook HTTP/1.1
Content-Type: application/json
X-RelySMS-Event: order.code_received
X-RelySMS-Delivery: cm…
X-RelySMS-Signature: t=1793095272,v1=5f2b…

{
  "id": "cm…",
  "event": "order.code_received",
  "createdAt": "2026-10-27T10:01:12.000Z",
  "order": { "id": 1042, "status": "received", "codes": [{ "code": "48213", … }], … }
}
```

每个请求都带有 X-RelySMS-Signature: t=<unix 时间>,v1=<签名>。签名是用你的 webhook 密钥对字符串 "t.body"（原始请求体，逐字节）计算的 HMAC-SHA256，十六进制。信任请求体之前，请校验签名并确认 t 在 5 分钟以内。

```python
import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

如果你的服务器没有返回 2xx，我们会在 10 秒、1 分钟、5 分钟、30 分钟、2 小时和 6 小时后重试——共 7 次，之后放弃。GET /webhook/deliveries 显示已发送和失败的投递。

### 接口

#### `GET /balance`

以美元计的余额，以及是否足以通过 API 购买。

```bash
curl -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/balance
```

响应:

```json
{ "balance": 25, "currency": "USD", "canBuy": true, "apiMinimum": 20 }
```

#### `GET /services`

所有在售服务：我们的 id、名称和 handler_api.php 代码。

| 参数 | 必填 | 说明 |
|---|---|---|
| `lang` (query) | 否 | 名称的语言：en（默认）、ru、uk、zh、es、pt、id、vi、tr、ar、fr。 |

```bash
curl -H "Authorization: Bearer $KEY" "https://relysms.pro/api/v1/services?lang=en"
```

响应:

```json
{ "services": [{ "id": "telegram", "name": "Telegram", "protocolCode": "tg" }] }
```

#### `GET /countries`

所有在售国家：ISO 代码、名称和 handler_api.php 代码。

| 参数 | 必填 | 说明 |
|---|---|---|
| `lang` (query) | 否 | 名称的语言：en（默认）、ru、uk、zh、es、pt、id、vi、tr、ar、fr。 |

```bash
curl -H "Authorization: Bearer $KEY" "https://relysms.pro/api/v1/countries?lang=en"
```

响应:

```json
{ "countries": [{ "id": "US", "name": "United States", "protocolCode": 187 }] }
```

#### `GET /prices`

以美元计的价格和大致号码数量。不带参数——所有在售项目。

| 参数 | 必填 | 说明 |
|---|---|---|
| `service` (query) | 否 | 来自 /services 的服务 id，例如 telegram。 |
| `country` (query) | 否 | 来自 /countries 的国家 ISO 代码，例如 US。 |

```bash
curl -H "Authorization: Bearer $KEY" "https://relysms.pro/api/v1/prices?service=telegram"
```

响应:

```json
{ "prices": [{ "service": "telegram", "country": "US", "price": 0.59, "stock": 1000 }] }
```

可能的错误: `unknown_service`, `unknown_country`

#### `POST /orders`

购买号码。立即扣费；未收到验证码则退款。

| 参数 | 必填 | 说明 |
|---|---|---|
| `service` (body) | 是 | 来自 /services 的服务 id，例如 telegram。 |
| `country` (body) | 是 | 来自 /countries 的国家 ISO 代码，例如 US。 |
| `maxPrice` (body) | 否 | 以美元计的最高价格，数字。超过时返回 price_above_max 及当前价格，不扣费。 |

```bash
curl -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c1f0e2a-run-1" \
  -d '{"service":"telegram","country":"US","maxPrice":0.8}' \
  https://relysms.pro/api/v1/orders
```

响应:

```json
{
  "order": {
    "id": 1042,
    "service": "telegram",
    "country": "US",
    "phone": "15550001111",
    "price": 0.59,
    "status": "waiting",
    "refunded": false,
    "codes": [],
    "awaitingAnotherCode": false,
    "createdAt": "2026-10-27T10:00:00.000Z",
    "expiresAt": "2026-10-27T10:20:00.000Z",
    "cancelableAt": "2026-10-27T10:02:00.000Z"
  }
}
```

可能的错误: `bad_request`, `unknown_service`, `unknown_country`, `no_numbers`, `insufficient_balance`, `below_api_minimum`, `price_above_max`, `too_many_open_orders`, `buying_too_fast`, `idempotency_key_reused`, `idempotency_in_progress`

#### `POST /orders/batch`

一次请求为同一服务和国家购买最多 20 个号码。

| 参数 | 必填 | 说明 |
|---|---|---|
| `service` (body) | 是 | 来自 /services 的服务 id，例如 telegram。 |
| `country` (body) | 是 | 来自 /countries 的国家 ISO 代码，例如 US。 |
| `quantity` (body) | 是 | 号码数量，1 到 20。 |
| `maxPrice` (body) | 否 | 以美元计的最高价格，数字。超过时返回 price_above_max 及当前价格，不扣费。 |

```bash
curl -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c1f0e2a-batch-1" \
  -d '{"service":"telegram","country":"US","quantity":5}' \
  https://relysms.pro/api/v1/orders/batch
```

响应:

```json
{
  "orders": [{ "id": 1043, "phone": "15550001112", … }, { "id": 1044, … }],
  "requested": 5,
  "placed": 2,
  "stoppedBy": { "code": "no_numbers", "message": "…" }
}
```

可能的错误: `bad_request`, `unknown_service`, `unknown_country`, `no_numbers`, `insufficient_balance`, `below_api_minimum`, `price_above_max`, `too_many_open_orders`, `buying_too_fast`, `idempotency_key_reused`, `idempotency_in_progress`

#### `GET /orders`

你的订单，最新的在前：默认为未结束的，也可以是全部。

| 参数 | 必填 | 说明 |
|---|---|---|
| `status` (query) | 否 | open（默认）——等待中或已有验证码；all——全部订单。 |
| `limit` (query) | 否 | 每页订单数，1 到 100（默认 50）。 |
| `before` (query) | 否 | 上一页的 next 值。 |

```bash
curl -H "Authorization: Bearer $KEY" "https://relysms.pro/api/v1/orders?status=all&limit=50"
```

响应:

```json
{ "orders": [{ "id": 1042, … }], "next": "1001" }
```

可能的错误: `bad_request`

#### `GET /orders/{id}`

单个订单及其验证码。状态为 waiting 时每 5–10 秒查询一次。

| 参数 | 必填 | 说明 |
|---|---|---|
| `id` (path) | 是 | 购买响应中的订单 id。 |

```bash
curl -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/orders/1042
```

响应:

```json
{
  "order": {
    "id": 1042,
    "status": "received",
    "codes": [{ "code": "48213", "text": "Telegram code: 48213", "receivedAt": "2026-10-27T10:01:12.000Z" }],
    …
  }
}
```

可能的错误: `not_found`

#### `POST /orders/{id}/finish`

完成已收到验证码的订单。号码将不再接收短信。重复调用无害。

| 参数 | 必填 | 说明 |
|---|---|---|
| `id` (path) | 是 | 购买响应中的订单 id。 |

```bash
curl -X POST -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/orders/1042/finish
```

响应:

```json
{ "order": { "id": 1042, "status": "finished", … } }
```

可能的错误: `not_found`, `invalid_state`

#### `POST /orders/{id}/another-code`

同一号码上的另一个验证码——收到第一个验证码后、订单未关闭时可用。免费。新验证码到达前，订单中 awaitingAnotherCode = true。

| 参数 | 必填 | 说明 |
|---|---|---|
| `id` (path) | 是 | 购买响应中的订单 id。 |

```bash
curl -X POST -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/orders/1042/another-code
```

响应:

```json
{ "order": { "id": 1042, "status": "received", "awaitingAnotherCode": true, … } }
```

可能的错误: `not_found`, `invalid_state`, `try_later`

#### `POST /orders/{id}/cancel`

取消未收到验证码的订单——购买 2 分钟后可取消。款项立即退回。

| 参数 | 必填 | 说明 |
|---|---|---|
| `id` (path) | 是 | 购买响应中的订单 id。 |

```bash
curl -X POST -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/orders/1042/cancel
```

响应:

```json
{ "order": { "id": 1042, "status": "cancelled", "refunded": true, … } }
```

可能的错误: `not_found`, `invalid_state`, `too_early_to_cancel`

#### `GET /webhook`

Webhook 地址及其签名密钥，或 null。

```bash
curl -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/webhook
```

响应:

```json
{ "webhook": { "url": "https://example.com/relysms-hook", "secret": "whsec_…", "createdAt": "2026-10-27T10:00:00.000Z" } }
```

#### `PUT /webhook`

设置或更改地址。密钥只生成一次并保留；rotateSecret 生成新密钥。

| 参数 | 必填 | 说明 |
|---|---|---|
| `url` (body) | 是 | 你服务器的 HTTPS 地址。私有和本地地址会被拒绝。 |
| `rotateSecret` (body) | 否 | true——生成新的签名密钥。 |

```bash
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/relysms-hook"}' \
  https://relysms.pro/api/v1/webhook
```

响应:

```json
{ "webhook": { "url": "https://example.com/relysms-hook", "secret": "whsec_…", "createdAt": "2026-10-27T10:00:00.000Z" } }
```

可能的错误: `bad_request`, `invalid_webhook_url`

#### `DELETE /webhook`

删除 webhook。尚未发送的事件将被丢弃。

```bash
curl -X DELETE -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/webhook
```

响应:

```json
{ "webhook": null }
```

#### `POST /webhook/test`

发送 ping 事件，检查地址和签名。

```bash
curl -X POST -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/webhook/test
```

响应:

```json
{ "delivery": { "id": "cm…" } }
```

可能的错误: `not_found`

#### `GET /webhook/deliveries`

最近 20 次投递：事件、状态、尝试次数、你的服务器最后的响应。

```bash
curl -H "Authorization: Bearer $KEY" https://relysms.pro/api/v1/webhook/deliveries
```

响应:

```json
{
  "deliveries": [{
    "id": "cm…", "event": "order.code_received", "orderId": 1042,
    "status": "pending", "attempts": 2, "lastHttpStatus": 502, "lastError": "HTTP 502",
    "createdAt": "2026-10-27T10:01:12.000Z", "deliveredAt": null, "nextAttemptAt": "2026-10-27T10:02:22.000Z"
  }]
}
```

### 订单状态

订单的 status 字段：

- `waiting` — 号码归你，尚未收到验证码。
- `received` — 已收到验证码，在 codes 中。订单开放期间可能还会收到更多验证码。
- `finished` — 你已完成订单（或收到验证码后时间已到）。
- `cancelled` — 未收到验证码即取消；refunded 为 true，款项已在余额中。
- `expired` — 时间到且未收到验证码；款项已退回余额。

### 错误

错误由 HTTP 状态码和 JSON 组成：code 供程序判断，message 供人阅读。部分错误带有额外字段（price、cancelableAt、apiMinimum）。

```json
{ "error": { "code": "price_above_max", "message": "The price is above maxPrice.", "price": 0.59 } }
```

- `missing_key` (HTTP 401) — Authorization 或 X-Api-Key 中没有密钥。
- `invalid_key` (HTTP 401) — 密钥错误、已被替换，或账户被封禁。
- `bad_request` (HTTP 400) — 请求体或参数有误；message 中会说明位置。
- `unknown_service` (HTTP 400) — 没有这个服务 id。
- `unknown_country` (HTTP 400) — 没有这个国家代码。
- `idempotency_key_invalid` (HTTP 400) — Idempotency-Key 为空或超过 255 个字符。
- `invalid_webhook_url` (HTTP 400) — Webhook 地址不是 HTTPS、不是有效地址，或指向私有/本地服务器。
- `insufficient_balance` (HTTP 402) — 余额低于号码价格。
- `below_api_minimum` (HTTP 402) — 余额不足 $20——API 仅在 $20 起可购买。
- `account_blocked` (HTTP 403) — 账户被封禁——请联系客服。
- `not_found` (HTTP 404) — 此密钥下没有该 id 的订单。
- `no_numbers` (HTTP 409) — 该组合暂时没有号码；未扣费。
- `price_above_max` (HTTP 409) — 价格高于 maxPrice；当前价格在 price 字段中。
- `too_many_open_orders` (HTTP 409) — 未结束订单过多——请完成或取消部分订单。
- `invalid_state` (HTTP 409) — 当前状态下无法执行（例如未收到验证码就完成）。
- `too_early_to_cancel` (HTTP 409) — 从 cancelableAt（购买 2 分钟后）起才可取消。
- `idempotency_in_progress` (HTTP 409) — 带此 Idempotency-Key 的请求仍在执行；请稍后重试。
- `idempotency_key_reused` (HTTP 422) — 此 Idempotency-Key 已用于另一个请求。
- `rate_limited` (HTTP 429) — 每秒超过 20 个请求；Retry-After 告诉你何时重试。
- `buying_too_fast` (HTTP 429) — 每秒超过 5 次购买；请使用 /orders/batch。
- `try_later` (HTTP 503) — 暂时失败——请一分钟后重试。

### REST 示例

购买号码、等待验证码、完成订单。

```python
import time, uuid, requests

BASE = "https://relysms.pro/api/v1"
S = requests.Session()
S.headers["Authorization"] = "Bearer rs_your_key"

r = S.post(f"{BASE}/orders",
           json={"service": "telegram", "country": "US", "maxPrice": 0.8},
           headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=30)
if r.status_code != 201:
    raise SystemExit(r.json()["error"])   # no_numbers, below_api_minimum, …
order = r.json()["order"]
print("number:", order["phone"])

while order["status"] == "waiting":       # up to 20 minutes
    time.sleep(10)
    order = S.get(f"{BASE}/orders/{order['id']}", timeout=30).json()["order"]

if order["status"] == "received":
    print("code:", order["codes"][-1]["code"])
    S.post(f"{BASE}/orders/{order['id']}/finish", timeout=30)
# "expired" or "cancelled": the money is back on the balance
```

```javascript
const BASE = "https://relysms.pro/api/v1";
const headers = { Authorization: "Bearer rs_your_key", "Content-Type": "application/json" };

const res = await fetch(`${BASE}/orders`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({ service: "telegram", country: "US", maxPrice: 0.8 }),
});
if (res.status !== 201) throw new Error((await res.json()).error.code);
let { order } = await res.json();
console.log("number:", order.phone);

while (order.status === "waiting") {
  await new Promise((r) => setTimeout(r, 10_000));
  ({ order } = await (await fetch(`${BASE}/orders/${order.id}`, { headers })).json());
}
if (order.status === "received") {
  console.log("code:", order.codes.at(-1).code);
  await fetch(`${BASE}/orders/${order.id}/finish`, { method: "POST", headers });
}
```

## 示例

完整流程：购买号码、等待验证码、完成订单。

```bash
KEY=rs_your_key
BASE=https://relysms.pro/stubs/handler_api.php

curl "$BASE?api_key=$KEY&action=getBalance"
curl "$BASE?api_key=$KEY&action=getNumber&service=tg&country=187&maxPrice=0.80"
# → ACCESS_NUMBER:1042:15550001111
curl "$BASE?api_key=$KEY&action=getStatus&id=1042"
# → STATUS_WAIT_CODE … then STATUS_OK:48213
curl "$BASE?api_key=$KEY&action=setStatus&id=1042&status=6"
```

```python
import time, requests

BASE = "https://relysms.pro/stubs/handler_api.php"
KEY = "rs_your_key"

def call(action, **params):
    r = requests.get(BASE, params={"api_key": KEY, "action": action, **params}, timeout=30)
    r.raise_for_status()
    return r.text

answer = call("getNumber", service="tg", country="187", maxPrice="0.80")
if not answer.startswith("ACCESS_NUMBER"):
    raise SystemExit(answer)          # NO_NUMBERS, NO_BALANCE, …
_, order_id, phone = answer.split(":")
print("number:", phone)

for _ in range(120):                  # up to 20 minutes, every 10 s
    status = call("getStatus", id=order_id)
    if status.startswith("STATUS_OK:"):
        print("code:", status.split(":", 1)[1])
        call("setStatus", id=order_id, status=6)
        break
    if status == "STATUS_CANCEL":
        break                         # refunded automatically
    time.sleep(10)
```

```javascript
const BASE = "https://relysms.pro/stubs/handler_api.php";
const KEY = "rs_your_key";

async function call(action, params = {}) {
  const url = new URL(BASE);
  url.search = new URLSearchParams({ api_key: KEY, action, ...params });
  const res = await fetch(url);
  return res.text();
}

const answer = await call("getNumber", { service: "tg", country: "187", maxPrice: "0.80" });
if (!answer.startsWith("ACCESS_NUMBER")) throw new Error(answer);
const [, id, phone] = answer.split(":");
console.log("number:", phone);

for (let i = 0; i < 120; i++) {
  const status = await call("getStatus", { id });
  if (status.startsWith("STATUS_OK:")) {
    console.log("code:", status.slice("STATUS_OK:".length));
    await call("setStatus", { id, status: "6" });
    break;
  }
  if (status === "STATUS_CANCEL") break; // refunded automatically
  await new Promise((r) => setTimeout(r, 10_000));
}
```

## 常见问题

**我的软件之前用的是别的接码服务，需要改什么？**

把服务器地址改为 https://relysms.pro/stubs/handler_api.php，密钥换成账户中的密钥。服务和国家代码保持不变。

**余额里有钱，为什么返回 NO_BALANCE？**

通过 API 购买需要余额不少于 $20。账户网页和 Mini App 没有这个门槛。

**没收到验证码，钱怎么办？**

时间窗口结束后自动退回余额；或在 2 分钟后取消时立即退回。

**能在同一号码上收第二条验证码吗？**

可以，在时间窗口内：调用 setStatus=3，然后继续 getStatus。

**有测试模式吗？**

没有：所有购买都是真实的。先用便宜的组合，在一个号码上验证流程。

**密钥泄露了怎么办？**

账户 →“余额”→“API”→“生成新密钥”。旧密钥立即失效。
