# RelySMS — API penerima SMS — untuk software Anda

Jika software Anda sudah bekerja dengan layanan aktivasi lewat handler_api.php, software itu bekerja dengan RelySMS: ganti alamat server dan kunci. Aksi sama, jawaban sama, harga dan refund kami.

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

## Mulai cepat

1. Masuk ke akun dan buka Saldo → API → Buat kunci. Kunci hanya ditampilkan sekali — simpan baik-baik.
2. Isi saldo hingga $20 atau lebih: pembelian lewat API hanya berjalan dengan saldo itu. Menyimpan kunci dengan saldo nol tidak masalah.
3. Atur server https://relysms.pro/stubs/handler_api.php di software Anda dan pakai kunci Anda sebagai ganti kunci layanan sebelumnya.
4. Beli nomor (getNumber), lalu cek status setiap 5–10 detik (getStatus) sampai muncul STATUS_OK beserta kodenya.
5. Selesaikan pesanan (setStatus=6). Jika kode tidak masuk, batalkan setelah 2 menit (setStatus=8) atau tunggu sampai jendela waktu habis: uang kembali otomatis.

## Aturan & batas

- Pembelian lewat API memerlukan saldo minimal $20. Di bawah itu, getNumber menjawab NO_BALANCE; metode lain tetap berjalan.
- Nomor milik Anda selama 20 menit. Tidak ada kode — pesanan tertutup sendiri dan uang kembali ke saldo.
- Pesanan bisa dibatalkan 2 menit setelah dibeli jika kode belum masuk. Uang langsung kembali.
- Begitu kode pertama masuk, pesanan dianggap lunas dan tidak dikembalikan. Selama jendela waktu masih terbuka, kode berikutnya masih bisa masuk.
- Maksimal 20 permintaan dan 5 pembelian per detik per kunci; maksimal 50 pesanan terbuka sekaligus.
- Harga dikunci saat pembelian. maxPrice mencegah pembelian di atas batas Anda.
- Ketentuan layanan berlaku: spam, penipuan, dan pendaftaran massal untuk tujuan itu dilarang.

## Metode

Setiap permintaan berupa GET atau POST ke satu alamat, dengan parameter di query string atau body formulir. Kunci dikirim di api_key, aksinya di action.

### `getBalance`

Saldo akun dalam dolar.

Hanya api_key dan action.

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

Jawaban:

```
ACCESS_BALANCE:25.00
```

### `getNumber`

Beli nomor untuk sebuah layanan di sebuah negara. Langsung ditagih; dikembalikan jika kode tidak masuk. Jawaban: ACCESS_NUMBER:nomor_pesanan:telepon.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `service` | ya | Kode layanan (getServicesList), misalnya tg. |
| `country` | ya | Kode negara (getCountries). |
| `maxPrice` | tidak | Harga maksimal dalam dolar. Jika lebih mahal — WRONG_MAX_PRICE beserta harga saat ini, tanpa tagihan. |

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

Jawaban:

```
ACCESS_NUMBER:1042:15550001111
```

Kemungkinan error: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`, `BANNED`

### `getNumberV2`

Sama seperti getNumber, dengan jawaban JSON berisi harga dan waktu jendela.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `service` | ya | Kode layanan. |
| `country` | ya | Kode negara. |
| `maxPrice` | tidak | Harga maksimal dalam dolar. |

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

Jawaban:

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

Kemungkinan error: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`

### `getStatus`

Status pesanan: menunggu kode, kode masuk (beserta kodenya), atau dibatalkan.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `id` | ya | Nomor pesanan dari getNumber. |

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

Jawaban:

```
STATUS_OK:48213
```

Kemungkinan error: `NO_ACTIVATION`

### `setStatus`

Tindakan atas pesanan: siap, kode lagi, selesai, batal (lihat Status).

| Parameter | Wajib | Keterangan |
|---|---|---|
| `id` | ya | Nomor pesanan. |
| `status` | ya | 1, 3, 6, atau 8. |

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

Jawaban:

```
ACCESS_ACTIVATION
```

Kemungkinan error: `NO_ACTIVATION`, `BAD_STATUS`, `EARLY_CANCEL_DENIED`, `ERROR_SQL`

### `getActiveActivations`

Semua pesanan terbuka milik kunci: nomor, status, kode dan teks yang masuk.

Hanya api_key dan action.

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

Jawaban:

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

Harga kami dalam dolar dan perkiraan jumlah nomor. Tanpa parameter — semua yang dijual.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `service` | tidak | Hanya layanan ini. |
| `country` | tidak | Hanya negara ini. |

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

Jawaban:

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

Kemungkinan error: `BAD_SERVICE`, `BAD_COUNTRY`

### `getCountries`

Daftar negara dengan kode protokol serta nama dalam bahasa Rusia dan Inggris.

Hanya api_key dan action.

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

Jawaban:

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

### `getServicesList`

Daftar layanan dengan kode protokol.

Hanya api_key dan action.

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

Jawaban:

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

## Status

Nilai status pada setStatus:

- `status=1` → `ACCESS_READY` — Nomor sudah dimasukkan ke layanan, menunggu kode. Tidak mengubah apa pun; dipertahankan demi kompatibilitas.
- `status=3` → `ACCESS_RETRY_GET` — Minta kode lain ke nomor yang sama — setelah kode pertama, selama pesanan terbuka. Gratis. Sampai kode itu datang, getStatus menjawab STATUS_WAIT_RETRY.
- `status=6` → `ACCESS_ACTIVATION` — Selesaikan pesanan setelah kode masuk. Tidak ada SMS baru yang akan masuk ke nomor itu.
- `status=8` → `ACCESS_CANCEL` — Batalkan pesanan tanpa kode (paling cepat setelah 2 menit). Uang kembali ke saldo.

Jawaban getStatus:

- `STATUS_WAIT_CODE` — Kode belum masuk — tanyakan lagi 5–10 detik kemudian.
- `STATUS_OK:48213` — Kode sudah masuk; setelah titik dua adalah kodenya (atau teks SMS jika tidak berisi kode).
- `STATUS_WAIT_RETRY:48213` — Kode lain sudah diminta dan belum datang; setelah titik dua adalah kode sebelumnya.
- `STATUS_CANCEL` — Dibatalkan atau ditutup tanpa kode; uang sudah kembali ke saldo.

## Error

Error dikirim sebagai teks biasa, seperti lazimnya protokol ini. Semua, kecuali RATE_LIMIT, dengan status 200.

- `NO_KEY` — api_key tidak dikirim.
- `BAD_KEY` — Kunci salah atau dinonaktifkan, atau akun diblokir.
- `BAD_ACTION` — Aksi tidak dikenal.
- `BAD_SERVICE` — Kode layanan tidak ada.
- `BAD_COUNTRY` — Kode negara tidak ada.
- `NO_NUMBERS` — Belum ada nomor untuk pasangan ini; tidak ada tagihan.
- `NO_BALANCE` — Saldo kurang dari $20 (batas API) atau kurang dari harga nomor.
- `WRONG_MAX_PRICE` — Harga melebihi maxPrice; setelah titik dua adalah harga saat ini.
- `LIMIT_ACTIVATIONS` — Terlalu banyak pesanan terbuka — selesaikan atau batalkan sebagian.
- `RATE_LIMIT` — Terlalu sering; HTTP 429. Coba lagi sedetik kemudian.
- `BANNED` — Akun diblokir — hubungi dukungan.
- `NO_ACTIVATION` — Tidak ada pesanan dengan nomor itu untuk kunci ini.
- `BAD_STATUS` — Aksi ini sedang tidak tersedia untuk pesanan tersebut (misalnya menyelesaikan tanpa kode).
- `EARLY_CANCEL_DENIED` — Pembatalan baru bisa 2 menit setelah pembelian.
- `ERROR_SQL` — Galat sementara di pihak kami — ulangi permintaan dalam semenit.

## Kode layanan & negara

Kodenya adalah kode standar protokol: tg — Telegram, wa — WhatsApp, dan seterusnya; negara 187 — AS. getServicesList dan getCountries memberi daftar lengkap — ambil kode dari sana, bukan dari tabel lama: layanan dan negara berubah sesuai stok.

## REST API v1

Nomor dan aturan yang sama dengan handler_api.php, dalam JSON: layanan dengan nama (telegram), negara dengan kode ISO (US), status HTTP, dan kode error yang mudah dipakai program untuk bercabang. Pakai ini untuk kode baru; biarkan handler_api.php untuk software yang sudah memakainya.

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

### Kunci

Kirim kunci di header: Authorization: Bearer <kunci> (atau X-Api-Key: <kunci>). Jangan di URL — URL tercatat di log. Batas, ambang $20, dan katalognya sama dengan handler_api.php.

### Ulangi tanpa pembelian ganda

Kirim header Idempotency-Key dengan nilai Anda sendiri (UUID, hingga 255 karakter) pada POST /orders dan /orders/batch. Jika koneksi putus dan Anda mengulang permintaan dengan kunci yang sama, Anda mendapat jawaban pertama (dengan header Idempotent-Replayed: true) dan tidak ada yang dibeli dua kali. Kunci yang sama dengan isi berbeda ditolak; pengulangan yang tiba saat yang pertama masih berjalan mendapat idempotency_in_progress. Jawaban sukses disimpan 24 jam; pembelian yang gagal tidak memotong apa pun dan bisa diulang dengan kunci yang sama.

### Beberapa nomor sekaligus

POST /orders/batch membeli hingga 20 nomor untuk satu layanan dan negara dalam satu permintaan — cara membeli lebih dari 5 per detik. Pembelian dilakukan satu per satu; yang pertama gagal menghentikan batch, dan jawabannya berisi yang sudah dibeli serta alasannya di stoppedBy. Jika tidak ada yang terbeli, jawabannya adalah error tersebut.

### Halaman

GET /orders mengembalikan yang terbaru lebih dulu. Jika next tidak null, kirim sebagai before untuk halaman berikutnya.

### Webhook

Daripada mengecek GET /orders/{id} terus, atur alamat HTTPS lewat PUT /webhook dan kami akan mengirim POST ke sana saat ada perubahan pada pesanan yang dibeli lewat API. Isinya adalah pesanan sebagaimana saat dikirim. Balas dengan 2xx apa pun dalam 10 detik; satu pesanan bisa menghasilkan beberapa event, dan event yang sama bisa datang dua kali setelah pengulangan, jadi gunakan id pengiriman.

- `order.code_received` — Kode datang (setiap kode baru mengirim ini lagi).
- `order.finished` — Pesanan selesai — oleh Anda, atau saat waktunya habis setelah ada kode.
- `order.cancelled` — Dibatalkan tanpa kode; uang kembali ke saldo.
- `order.expired` — Waktu habis tanpa kode; uang kembali ke saldo.
- `ping` — Uji dari POST /webhook/test; order bernilai 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", … }], … }
}
```

Setiap permintaan membawa X-RelySMS-Signature: t=<waktu unix>,v1=<tanda tangan>. Tanda tangannya adalah HMAC-SHA256 dari string "t.body" (body mentah, byte demi byte) dengan secret webhook Anda, dalam hex. Periksa tanda tangan itu, dan bahwa t tidak lebih dari 5 menit, sebelum memercayai isinya.

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

Jika server Anda tidak membalas 2xx, kami mengulang setelah 10 detik, 1 menit, 5 menit, 30 menit, 2 jam, dan 6 jam — total 7 percobaan — lalu berhenti. GET /webhook/deliveries menunjukkan apa yang terkirim dan yang gagal.

### Endpoint

#### `GET /balance`

Saldo dalam dolar, dan apakah cukup untuk membeli lewat API.

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

Jawaban:

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

#### `GET /services`

Semua layanan yang dijual: id kami, nama, dan kode handler_api.php.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `lang` (query) | tidak | Bahasa nama: en (bawaan), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Jawaban:

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

#### `GET /countries`

Semua negara yang dijual: kode ISO, nama, dan kode handler_api.php.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `lang` (query) | tidak | Bahasa nama: en (bawaan), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Jawaban:

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

#### `GET /prices`

Harga kami dalam dolar dan perkiraan jumlah nomor. Tanpa parameter — semua yang dijual.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `service` (query) | tidak | Id layanan dari /services, misalnya telegram. |
| `country` (query) | tidak | Kode ISO negara dari /countries, misalnya US. |

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

Jawaban:

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

Kemungkinan error: `unknown_service`, `unknown_country`

#### `POST /orders`

Beli nomor. Saldo langsung dipotong dan dikembalikan jika kode tidak datang.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `service` (body) | ya | Id layanan dari /services, misalnya telegram. |
| `country` (body) | ya | Kode ISO negara dari /countries, misalnya US. |
| `maxPrice` (body) | tidak | Harga tertinggi dalam dolar, berupa angka. Di atasnya — price_above_max dengan harga saat ini, tidak ada potongan. |

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

Jawaban:

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

Kemungkinan error: `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`

Beli hingga 20 nomor untuk satu layanan dan negara dalam satu permintaan.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `service` (body) | ya | Id layanan dari /services, misalnya telegram. |
| `country` (body) | ya | Kode ISO negara dari /countries, misalnya US. |
| `quantity` (body) | ya | Berapa nomor, 1 sampai 20. |
| `maxPrice` (body) | tidak | Harga tertinggi dalam dolar, berupa angka. Di atasnya — price_above_max dengan harga saat ini, tidak ada potongan. |

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

Jawaban:

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

Kemungkinan error: `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`

Pesanan Anda, terbaru lebih dulu: yang terbuka secara bawaan, atau semua.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `status` (query) | tidak | open (bawaan) — menunggu atau sudah ada kode; all — semua pesanan. |
| `limit` (query) | tidak | Pesanan per halaman, 1 sampai 100 (bawaan 50). |
| `before` (query) | tidak | Nilai next dari halaman sebelumnya. |

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

Jawaban:

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

Kemungkinan error: `bad_request`

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

Satu pesanan dengan kodenya. Cek setiap 5–10 detik selama statusnya waiting.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `id` (path) | ya | Id pesanan dari jawaban pembelian. |

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

Jawaban:

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

Kemungkinan error: `not_found`

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

Selesaikan pesanan yang sudah ada kodenya. Nomor tidak akan menerima SMS lagi. Mengulang tidak berbahaya.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `id` (path) | ya | Id pesanan dari jawaban pembelian. |

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

Jawaban:

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

Kemungkinan error: `not_found`, `invalid_state`

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

Kode lain ke nomor yang sama — setelah kode pertama, selama pesanan terbuka. Gratis. Sampai kode itu datang, pesanan memiliki awaitingAnotherCode = true.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `id` (path) | ya | Id pesanan dari jawaban pembelian. |

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

Jawaban:

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

Kemungkinan error: `not_found`, `invalid_state`, `try_later`

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

Batalkan pesanan tanpa kode, mulai 2 menit setelah pembelian. Uang langsung kembali.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `id` (path) | ya | Id pesanan dari jawaban pembelian. |

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

Jawaban:

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

Kemungkinan error: `not_found`, `invalid_state`, `too_early_to_cancel`

#### `GET /webhook`

Alamat webhook dan secret penandatanganannya, atau null.

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

Jawaban:

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

#### `PUT /webhook`

Atur atau ubah alamat. Secret dibuat sekali dan disimpan; rotateSecret membuat yang baru.

| Parameter | Wajib | Keterangan |
|---|---|---|
| `url` (body) | ya | Alamat HTTPS server Anda. Alamat privat dan lokal ditolak. |
| `rotateSecret` (body) | tidak | true — buat secret penandatanganan baru. |

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

Jawaban:

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

Kemungkinan error: `bad_request`, `invalid_webhook_url`

#### `DELETE /webhook`

Hapus webhook. Event yang belum terkirim dibuang.

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

Jawaban:

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

#### `POST /webhook/test`

Kirim event ping untuk memeriksa alamat dan tanda tangan.

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

Jawaban:

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

Kemungkinan error: `not_found`

#### `GET /webhook/deliveries`

20 pengiriman terakhir: event, status, percobaan, jawaban terakhir server Anda.

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

Jawaban:

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

Kolom status pada pesanan:

- `waiting` — Nomor milik Anda dan belum ada kode.
- `received` — Kode sudah datang; ada di codes. Kode lain bisa datang selama pesanan terbuka.
- `finished` — Anda menyelesaikan pesanan (atau waktunya habis setelah ada kode).
- `cancelled` — Dibatalkan tanpa kode; refunded bernilai true dan uang ada di saldo.
- `expired` — Waktu habis tanpa kode; uang kembali ke saldo.

### Error

Error berupa status HTTP dan JSON dengan code untuk program dan message untuk manusia. Sebagian membawa kolom tambahan (price, cancelableAt, apiMinimum).

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

- `missing_key` (HTTP 401) — Tidak ada kunci di Authorization atau X-Api-Key.
- `invalid_key` (HTTP 401) — Kunci salah, sudah diganti, atau akun diblokir.
- `bad_request` (HTTP 400) — Isi atau parameter salah; message menyebutkan yang mana.
- `unknown_service` (HTTP 400) — Tidak ada id layanan seperti itu.
- `unknown_country` (HTTP 400) — Tidak ada kode negara seperti itu.
- `idempotency_key_invalid` (HTTP 400) — Idempotency-Key kosong atau lebih dari 255 karakter.
- `invalid_webhook_url` (HTTP 400) — Alamat webhook bukan HTTPS, bukan URL, atau mengarah ke server privat atau lokal.
- `insufficient_balance` (HTTP 402) — Saldo di bawah harga.
- `below_api_minimum` (HTTP 402) — Saldo di bawah $20 — API hanya membeli mulai $20.
- `account_blocked` (HTTP 403) — Akun diblokir — hubungi dukungan.
- `not_found` (HTTP 404) — Tidak ada pesanan dengan id ini untuk kunci ini.
- `no_numbers` (HTTP 409) — Belum ada nomor untuk pasangan ini; tidak ada potongan.
- `price_above_max` (HTTP 409) — Harga melebihi maxPrice; price berisi harga saat ini.
- `too_many_open_orders` (HTTP 409) — Terlalu banyak pesanan terbuka — selesaikan atau batalkan sebagian.
- `invalid_state` (HTTP 409) — Tidak bisa pada status ini (misalnya menyelesaikan tanpa kode).
- `too_early_to_cancel` (HTTP 409) — Pembatalan bisa mulai cancelableAt, 2 menit setelah pembelian.
- `idempotency_in_progress` (HTTP 409) — Permintaan dengan Idempotency-Key ini masih berjalan; ulangi sebentar lagi.
- `idempotency_key_reused` (HTTP 422) — Idempotency-Key ini sudah dipakai untuk permintaan lain.
- `rate_limited` (HTTP 429) — Lebih dari 20 permintaan per detik; Retry-After memberi tahu kapan mengulang.
- `buying_too_fast` (HTTP 429) — Lebih dari 5 pembelian per detik; pakai /orders/batch.
- `try_later` (HTTP 503) — Untuk sementara gagal — coba lagi dalam semenit.

### Contoh REST

Beli nomor, tunggu kode, selesaikan pesanan.

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

## Contoh

Siklus lengkap: beli nomor, tunggu kode, selesaikan pesanan.

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

## Pertanyaan

**Software saya dulu memakai layanan aktivasi lain. Apa yang perlu diganti?**

Alamat server menjadi https://relysms.pro/stubs/handler_api.php dan kuncinya menjadi kunci dari akun Anda. Kode layanan dan negara tetap sama.

**Kenapa NO_BALANCE padahal ada saldo?**

Pembelian lewat API memerlukan saldo $20 atau lebih. Di akun dan Mini App tidak ada batas seperti itu.

**Bagaimana uangnya jika kode tidak masuk?**

Kembali otomatis ke saldo saat jendela waktu tertutup, atau langsung jika Anda membatalkan setelah 2 menit.

**Bisa dapat kode kedua di nomor yang sama?**

Bisa, selama jendela waktu masih terbuka: setStatus=3, lalu getStatus lagi.

**Ada mode uji coba?**

Tidak: semua pembelian nyata. Mulailah dengan pasangan yang murah dan uji alurnya pada satu nomor.

**Kunci saya bocor. Apa yang harus dilakukan?**

Akun → Saldo → API → Terbitkan yang baru. Kunci lama langsung tidak berlaku.
