# RelySMS — API nhận SMS — cho phần mềm của bạn

Nếu phần mềm của bạn đã chạy với một dịch vụ kích hoạt qua handler_api.php, nó sẽ chạy với RelySMS: chỉ cần đổi địa chỉ máy chủ và khóa. Cùng thao tác, cùng phản hồi, theo giá và chính sách hoàn tiền của chúng tôi.

- Địa chỉ: `https://relysms.pro/stubs/handler_api.php`
- Auth: `api_key` parameter
- Format: plain text tokens, JSON where noted

## Bắt đầu nhanh

1. Đăng nhập và mở Số dư → API → Tạo khóa. Khóa chỉ hiển thị một lần — hãy lưu lại.
2. Nạp số dư từ $20 trở lên: chỉ với số dư này mới mua được qua API. Giữ khóa khi số dư bằng 0 vẫn được.
3. Trong phần mềm, đặt máy chủ là https://relysms.pro/stubs/handler_api.php và dùng khóa của bạn thay cho khóa của dịch vụ cũ.
4. Mua số (getNumber), rồi hỏi trạng thái mỗi 5–10 giây (getStatus) cho đến khi nhận STATUS_OK kèm mã.
5. Hoàn tất đơn (setStatus=6). Nếu không có mã, hủy sau 2 phút (setStatus=8) hoặc chờ hết thời gian: tiền tự hoàn lại.

## Quy tắc và giới hạn

- Mua qua API cần số dư tối thiểu $20. Dưới mức đó, getNumber trả NO_BALANCE; các phương thức khác vẫn hoạt động.
- Số là của bạn trong 20 phút. Không có mã — đơn tự đóng và tiền về lại số dư.
- Có thể hủy sau 2 phút kể từ lúc mua nếu chưa có mã. Tiền về ngay.
- Khi mã đầu tiên đến, đơn được tính là đã trả và không hoàn lại. Trong thời gian còn mở, các mã tiếp theo vẫn có thể đến.
- Tối đa 20 yêu cầu và 5 lượt mua mỗi giây cho một khóa; tối đa 50 đơn đang mở cùng lúc.
- Giá được chốt lúc mua. maxPrice giúp không mua quá giới hạn của bạn.
- Áp dụng điều khoản dịch vụ: cấm spam, lừa đảo và đăng ký hàng loạt vì các mục đích đó.

## Phương thức

Mọi yêu cầu là GET hoặc POST tới một địa chỉ, tham số nằm trong query string hoặc form. Khóa gửi trong api_key, thao tác trong action.

### `getBalance`

Số dư tài khoản tính bằng đô la.

Chỉ api_key và action.

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

Phản hồi:

```
ACCESS_BALANCE:25.00
```

### `getNumber`

Mua số cho một dịch vụ ở một quốc gia. Trừ tiền ngay; hoàn lại nếu không có mã. Phản hồi: ACCESS_NUMBER:mã_đơn:số_điện_thoại.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `service` | có | Mã dịch vụ (getServicesList), ví dụ tg. |
| `country` | có | Mã quốc gia (getCountries). |
| `maxPrice` | không | Giá tối đa bằng đô la. Đắt hơn — WRONG_MAX_PRICE kèm giá hiện tại, không trừ tiền. |

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

Phản hồi:

```
ACCESS_NUMBER:1042:15550001111
```

Lỗi có thể gặp: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`, `BANNED`

### `getNumberV2`

Giống getNumber, nhưng trả JSON gồm giá và thời gian mở.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `service` | có | Mã dịch vụ. |
| `country` | có | Mã quốc gia. |
| `maxPrice` | không | Giá tối đa bằng đô la. |

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

Phản hồi:

```
{
  "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"
}
```

Lỗi có thể gặp: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`

### `getStatus`

Trạng thái đơn: chờ mã, đã có mã (kèm mã) hoặc đã hủy.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `id` | có | Mã đơn từ getNumber. |

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

Phản hồi:

```
STATUS_OK:48213
```

Lỗi có thể gặp: `NO_ACTIVATION`

### `setStatus`

Thao tác với đơn: sẵn sàng, mã khác, hoàn tất, hủy (xem Trạng thái).

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `id` | có | Mã đơn. |
| `status` | có | 1, 3, 6 hoặc 8. |

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

Phản hồi:

```
ACCESS_ACTIVATION
```

Lỗi có thể gặp: `NO_ACTIVATION`, `BAD_STATUS`, `EARLY_CANCEL_DENIED`, `ERROR_SQL`

### `getActiveActivations`

Mọi đơn đang mở của khóa: số, trạng thái, mã và nội dung đã nhận.

Chỉ api_key và action.

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

Phản hồi:

```
{
  "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`

Giá của chúng tôi bằng đô la và số lượng số ước tính. Không tham số — mọi thứ đang bán.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `service` | không | Chỉ dịch vụ này. |
| `country` | không | Chỉ quốc gia này. |

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

Phản hồi:

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

Lỗi có thể gặp: `BAD_SERVICE`, `BAD_COUNTRY`

### `getCountries`

Danh sách quốc gia với mã giao thức và tên tiếng Nga, tiếng Anh.

Chỉ api_key và action.

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

Phản hồi:

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

### `getServicesList`

Danh sách dịch vụ với mã giao thức.

Chỉ api_key và action.

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

Phản hồi:

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

## Trạng thái

Giá trị status trong setStatus:

- `status=1` → `ACCESS_READY` — Đã nhập số vào dịch vụ, đang chờ mã. Không thay đổi gì; giữ để tương thích.
- `status=3` → `ACCESS_RETRY_GET` — Yêu cầu thêm một mã cho cùng số — sau mã đầu tiên, khi đơn còn mở. Miễn phí. Cho đến khi mã mới đến, getStatus trả về STATUS_WAIT_RETRY.
- `status=6` → `ACCESS_ACTIVATION` — Hoàn tất đơn sau khi có mã. Số sẽ không nhận thêm SMS.
- `status=8` → `ACCESS_CANCEL` — Hủy đơn chưa có mã (sớm nhất sau 2 phút). Tiền về lại số dư.

Phản hồi của getStatus:

- `STATUS_WAIT_CODE` — Chưa có mã — hỏi lại sau 5–10 giây.
- `STATUS_OK:48213` — Đã có mã; sau dấu hai chấm là mã (hoặc nội dung SMS nếu không chứa mã).
- `STATUS_WAIT_RETRY:48213` — Đã yêu cầu thêm mã nhưng chưa đến; sau dấu hai chấm là mã trước đó.
- `STATUS_CANCEL` — Đã hủy hoặc đóng khi chưa có mã; tiền đã về số dư.

## Lỗi

Lỗi trả về dạng chuỗi, như thông lệ của giao thức này. Tất cả, trừ RATE_LIMIT, đều có mã trạng thái 200.

- `NO_KEY` — Thiếu api_key.
- `BAD_KEY` — Khóa sai, đã tắt, hoặc tài khoản bị khóa.
- `BAD_ACTION` — Thao tác không xác định.
- `BAD_SERVICE` — Không có mã dịch vụ này.
- `BAD_COUNTRY` — Không có mã quốc gia này.
- `NO_NUMBERS` — Hiện không có số cho cặp này; không trừ tiền.
- `NO_BALANCE` — Số dư dưới $20 (ngưỡng API) hoặc thấp hơn giá số.
- `WRONG_MAX_PRICE` — Giá vượt maxPrice; sau dấu hai chấm là giá hiện tại.
- `LIMIT_ACTIVATIONS` — Quá nhiều đơn đang mở — hãy hoàn tất hoặc hủy bớt.
- `RATE_LIMIT` — Quá thường xuyên; HTTP 429. Thử lại sau một giây.
- `BANNED` — Tài khoản bị khóa — hãy liên hệ hỗ trợ.
- `NO_ACTIVATION` — Không có đơn với mã này cho khóa này.
- `BAD_STATUS` — Thao tác này hiện không áp dụng cho đơn (ví dụ hoàn tất khi chưa có mã).
- `EARLY_CANCEL_DENIED` — Chỉ hủy được sau 2 phút kể từ lúc mua.
- `ERROR_SQL` — Lỗi tạm thời phía chúng tôi — hãy gửi lại sau một phút.

## Mã dịch vụ và quốc gia

Các mã là mã chuẩn của giao thức: tg — Telegram, wa — WhatsApp, v.v.; quốc gia 187 — Mỹ. getServicesList và getCountries trả danh sách đầy đủ — hãy lấy mã từ đó, không dùng bảng cũ: dịch vụ và quốc gia thay đổi theo hàng có sẵn.

## REST API v1

Cùng số và cùng quy tắc như handler_api.php, nhưng dạng JSON: dịch vụ theo tên (telegram), quốc gia theo mã ISO (US), mã trạng thái HTTP và mã lỗi để chương trình dễ rẽ nhánh. Dùng cho mã mới; phần mềm đã dùng handler_api.php thì cứ giữ nguyên.

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

### Khóa

Gửi khóa trong header: Authorization: Bearer <khóa> (hoặc X-Api-Key: <khóa>). Không đặt trong URL — URL bị ghi vào log. Giới hạn, ngưỡng $20 và danh mục giống handler_api.php.

### Thử lại mà không mua trùng

Thêm header Idempotency-Key với giá trị của bạn (UUID, tối đa 255 ký tự) vào POST /orders và /orders/batch. Nếu mất kết nối và bạn gửi lại với cùng khóa, bạn nhận lại phản hồi đầu tiên (kèm header Idempotent-Replayed: true) và không có gì bị mua hai lần. Cùng khóa nhưng nội dung khác sẽ bị từ chối; yêu cầu lặp đến khi yêu cầu đầu còn đang chạy sẽ nhận idempotency_in_progress. Phản hồi thành công được giữ 24 giờ; lượt mua thất bại không trừ tiền và có thể gửi lại với cùng khóa.

### Nhiều số cùng lúc

POST /orders/batch mua tối đa 20 số cho một dịch vụ và quốc gia trong một yêu cầu — đây là cách mua nhanh hơn 5 số mỗi giây. Các lượt mua diễn ra lần lượt; lượt đầu tiên thất bại sẽ dừng cả lô, phản hồi liệt kê những gì đã mua và lý do trong stoppedBy. Nếu không mua được gì, phản hồi chính là lỗi đó.

### Phân trang

GET /orders trả đơn mới nhất trước. Khi next khác null, gửi nó làm before để lấy trang tiếp theo.

### Webhook

Thay vì hỏi GET /orders/{id} liên tục, hãy đặt một địa chỉ HTTPS qua PUT /webhook và chúng tôi sẽ gửi POST tới đó khi đơn mua qua API có thay đổi. Nội dung là đơn hàng tại thời điểm gửi. Hãy trả lời bất kỳ mã 2xx nào trong 10 giây; một đơn có thể sinh nhiều sự kiện và cùng một sự kiện có thể đến hai lần sau khi thử lại, nên hãy dựa vào id lần gửi.

- `order.code_received` — Có mã (mỗi mã mới lại gửi sự kiện này).
- `order.finished` — Đơn đã hoàn tất — do bạn, hoặc khi hết thời gian sau khi có mã.
- `order.cancelled` — Đã hủy khi chưa có mã; tiền đã về số dư.
- `order.expired` — Hết thời gian mà không có mã; tiền đã về số dư.
- `ping` — Thử nghiệm từ POST /webhook/test; order là 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", … }], … }
}
```

Mỗi yêu cầu có X-RelySMS-Signature: t=<thời gian unix>,v1=<chữ ký>. Chữ ký là HMAC-SHA256 của chuỗi "t.body" (body gốc, từng byte) với secret webhook của bạn, dạng hex. Hãy kiểm tra chữ ký và t không quá 5 phút trước khi tin nội dung.

```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));
}
```

Nếu máy chủ của bạn không trả 2xx, chúng tôi thử lại sau 10 giây, 1 phút, 5 phút, 30 phút, 2 giờ và 6 giờ — tổng cộng 7 lần — rồi dừng. GET /webhook/deliveries cho thấy những gì đã gửi và đã lỗi.

### Endpoint

#### `GET /balance`

Số dư bằng đô la, và có đủ để mua qua API hay không.

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

Phản hồi:

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

#### `GET /services`

Mọi dịch vụ đang bán: id của chúng tôi, tên và mã handler_api.php.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `lang` (query) | không | Ngôn ngữ của tên: en (mặc định), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Phản hồi:

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

#### `GET /countries`

Mọi quốc gia đang bán: mã ISO, tên và mã handler_api.php.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `lang` (query) | không | Ngôn ngữ của tên: en (mặc định), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Phản hồi:

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

#### `GET /prices`

Giá của chúng tôi bằng đô la và số lượng số ước tính. Không tham số — mọi thứ đang bán.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `service` (query) | không | Id dịch vụ từ /services, ví dụ telegram. |
| `country` (query) | không | Mã ISO quốc gia từ /countries, ví dụ US. |

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

Phản hồi:

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

Lỗi có thể gặp: `unknown_service`, `unknown_country`

#### `POST /orders`

Mua số. Trừ tiền ngay; hoàn lại nếu không có mã.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `service` (body) | có | Id dịch vụ từ /services, ví dụ telegram. |
| `country` (body) | có | Mã ISO quốc gia từ /countries, ví dụ US. |
| `maxPrice` (body) | không | Giá tối đa bằng đô la, dạng số. Cao hơn — price_above_max kèm giá hiện tại, không trừ tiền. |

```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
```

Phản hồi:

```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"
  }
}
```

Lỗi có thể gặp: `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`

Mua tối đa 20 số cho một dịch vụ và quốc gia trong một yêu cầu.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `service` (body) | có | Id dịch vụ từ /services, ví dụ telegram. |
| `country` (body) | có | Mã ISO quốc gia từ /countries, ví dụ US. |
| `quantity` (body) | có | Số lượng số, từ 1 đến 20. |
| `maxPrice` (body) | không | Giá tối đa bằng đô la, dạng số. Cao hơn — price_above_max kèm giá hiện tại, không trừ tiền. |

```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
```

Phản hồi:

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

Lỗi có thể gặp: `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`

Các đơn của bạn, mới nhất trước: mặc định là đơn đang mở, hoặc tất cả.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `status` (query) | không | open (mặc định) — đang chờ hoặc đã có mã; all — mọi đơn. |
| `limit` (query) | không | Số đơn mỗi trang, từ 1 đến 100 (mặc định 50). |
| `before` (query) | không | Giá trị next của trang trước. |

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

Phản hồi:

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

Lỗi có thể gặp: `bad_request`

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

Một đơn kèm mã. Hỏi mỗi 5–10 giây khi trạng thái là waiting.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `id` (path) | có | Id đơn trong phản hồi mua. |

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

Phản hồi:

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

Lỗi có thể gặp: `not_found`

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

Hoàn tất đơn đã có mã. Số sẽ không nhận thêm SMS. Gọi lại không sao.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `id` (path) | có | Id đơn trong phản hồi mua. |

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

Phản hồi:

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

Lỗi có thể gặp: `not_found`, `invalid_state`

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

Thêm một mã cho cùng số — sau mã đầu tiên, khi đơn còn mở. Miễn phí. Cho đến khi mã mới đến, đơn có awaitingAnotherCode = true.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `id` (path) | có | Id đơn trong phản hồi mua. |

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

Phản hồi:

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

Lỗi có thể gặp: `not_found`, `invalid_state`, `try_later`

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

Hủy đơn chưa có mã, từ 2 phút sau khi mua. Tiền về ngay.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `id` (path) | có | Id đơn trong phản hồi mua. |

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

Phản hồi:

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

Lỗi có thể gặp: `not_found`, `invalid_state`, `too_early_to_cancel`

#### `GET /webhook`

Địa chỉ webhook và secret ký, hoặc null.

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

Phản hồi:

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

#### `PUT /webhook`

Đặt hoặc đổi địa chỉ. Secret được tạo một lần và giữ nguyên; rotateSecret tạo secret mới.

| Tham số | Bắt buộc | Mô tả |
|---|---|---|
| `url` (body) | có | Địa chỉ HTTPS của máy chủ bạn. Địa chỉ riêng và cục bộ bị từ chối. |
| `rotateSecret` (body) | không | true — tạo secret ký mới. |

```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
```

Phản hồi:

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

Lỗi có thể gặp: `bad_request`, `invalid_webhook_url`

#### `DELETE /webhook`

Xóa webhook. Sự kiện chưa gửi sẽ bị bỏ.

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

Phản hồi:

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

#### `POST /webhook/test`

Gửi sự kiện ping để kiểm tra địa chỉ và chữ ký.

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

Phản hồi:

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

Lỗi có thể gặp: `not_found`

#### `GET /webhook/deliveries`

20 lần gửi gần nhất: sự kiện, trạng thái, số lần thử, phản hồi cuối của máy chủ bạn.

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

Phản hồi:

```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"
  }]
}
```

### Trạng thái đơn

Trường status của đơn:

- `waiting` — Số là của bạn, chưa có mã.
- `received` — Đã có mã, nằm trong codes. Có thể có thêm mã khi đơn còn mở.
- `finished` — Bạn đã hoàn tất đơn (hoặc hết thời gian sau khi có mã).
- `cancelled` — Đã hủy khi chưa có mã; refunded là true và tiền đã ở số dư.
- `expired` — Hết thời gian mà không có mã; tiền đã về số dư.

### Lỗi

Lỗi gồm mã trạng thái HTTP và JSON có code để chương trình xử lý và message cho người đọc. Một số có thêm trường (price, cancelableAt, apiMinimum).

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

- `missing_key` (HTTP 401) — Không có khóa trong Authorization hoặc X-Api-Key.
- `invalid_key` (HTTP 401) — Khóa sai, đã bị thay, hoặc tài khoản bị khóa.
- `bad_request` (HTTP 400) — Nội dung hoặc tham số sai; message cho biết chỗ nào.
- `unknown_service` (HTTP 400) — Không có id dịch vụ này.
- `unknown_country` (HTTP 400) — Không có mã quốc gia này.
- `idempotency_key_invalid` (HTTP 400) — Idempotency-Key trống hoặc dài hơn 255 ký tự.
- `invalid_webhook_url` (HTTP 400) — Địa chỉ webhook không phải HTTPS, không phải URL, hoặc trỏ tới máy chủ riêng hay cục bộ.
- `insufficient_balance` (HTTP 402) — Số dư thấp hơn giá.
- `below_api_minimum` (HTTP 402) — Số dư dưới $20 — API chỉ mua khi từ $20.
- `account_blocked` (HTTP 403) — Tài khoản bị khóa — hãy liên hệ hỗ trợ.
- `not_found` (HTTP 404) — Không có đơn với id này cho khóa này.
- `no_numbers` (HTTP 409) — Hiện không có số cho cặp này; không trừ tiền.
- `price_above_max` (HTTP 409) — Giá vượt maxPrice; price chứa giá hiện tại.
- `too_many_open_orders` (HTTP 409) — Quá nhiều đơn đang mở — hãy hoàn tất hoặc hủy bớt.
- `invalid_state` (HTTP 409) — Không thể ở trạng thái này (ví dụ hoàn tất khi chưa có mã).
- `too_early_to_cancel` (HTTP 409) — Có thể hủy từ cancelableAt, 2 phút sau khi mua.
- `idempotency_in_progress` (HTTP 409) — Yêu cầu với Idempotency-Key này vẫn đang chạy; thử lại sau giây lát.
- `idempotency_key_reused` (HTTP 422) — Idempotency-Key này đã dùng cho một yêu cầu khác.
- `rate_limited` (HTTP 429) — Hơn 20 yêu cầu mỗi giây; Retry-After cho biết khi nào thử lại.
- `buying_too_fast` (HTTP 429) — Hơn 5 lượt mua mỗi giây; hãy dùng /orders/batch.
- `try_later` (HTTP 503) — Tạm thời không thành công — thử lại sau một phút.

### Ví dụ REST

Mua số, chờ mã, hoàn tất đơn.

```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 });
}
```

## Ví dụ

Chu trình đầy đủ: mua số, chờ mã, hoàn tất đơn.

```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));
}
```

## Câu hỏi

**Phần mềm của tôi từng dùng dịch vụ kích hoạt khác. Cần đổi gì?**

Đổi địa chỉ máy chủ thành https://relysms.pro/stubs/handler_api.php và khóa thành khóa trong tài khoản của bạn. Mã dịch vụ và quốc gia giữ nguyên.

**Vì sao NO_BALANCE dù vẫn còn tiền?**

Mua qua API cần số dư từ $20 trở lên. Tài khoản web và Mini App không có ngưỡng này.

**Tiền sẽ ra sao nếu không có mã?**

Tự về số dư khi hết thời gian, hoặc về ngay nếu bạn hủy sau 2 phút.

**Có nhận được mã thứ hai trên cùng số không?**

Có, khi còn thời gian: gọi setStatus=3 rồi getStatus tiếp.

**Có chế độ thử không?**

Không: mọi lượt mua đều thật. Hãy bắt đầu với cặp rẻ và thử quy trình trên một số.

**Khóa bị lộ thì làm sao?**

Tài khoản → Số dư → API → Cấp khóa mới. Khóa cũ ngừng hoạt động ngay.
