# RelySMS — API para recibir SMS — para tu propio software

Si tu software ya funciona con un servicio de activaciones mediante handler_api.php, funciona con RelySMS: cambia la dirección del servidor y la clave. Mismas acciones, mismas respuestas, nuestros precios y reembolsos.

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

## Inicio rápido

1. Entra en tu cuenta y abre Saldo → API → Crear clave. La clave se muestra una sola vez: guárdala.
2. Recarga hasta $20 o más: las compras por API solo pasan con ese saldo. Puedes tener la clave con saldo cero.
3. Configura en tu software el servidor https://relysms.pro/stubs/handler_api.php y tu clave en lugar de la del servicio anterior.
4. Compra un número (getNumber) y consulta el estado cada 5–10 segundos (getStatus) hasta recibir STATUS_OK con el código.
5. Termina el pedido (setStatus=6). Si no llega código, cancela a los 2 minutos (setStatus=8) o deja que termine la ventana: el dinero vuelve solo.

## Reglas y límites

- Las compras por API requieren un saldo de al menos $20. Por debajo, getNumber responde NO_BALANCE; los demás métodos funcionan.
- El número es tuyo 20 minutos. Sin código, el pedido se cierra solo y el dinero vuelve a tu saldo.
- Puedes cancelar 2 minutos después de comprar si no ha llegado código. El dinero vuelve al instante.
- Con el primer código el pedido queda pagado y no se reembolsa. Mientras la ventana siga abierta pueden llegar más códigos.
- Hasta 20 peticiones y 5 compras por segundo por clave; hasta 50 pedidos abiertos a la vez.
- El precio se fija al comprar. maxPrice evita comprar por encima de tu límite.
- Se aplican las condiciones del servicio: prohibidos el spam, el fraude y los registros masivos con esos fines.

## Métodos

Cada petición es un GET o POST a una sola dirección, con parámetros en la URL o en un formulario. La clave va en api_key y la acción en action.

### `getBalance`

El saldo de la cuenta en dólares.

Solo api_key y action.

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

Respuesta:

```
ACCESS_BALANCE:25.00
```

### `getNumber`

Comprar un número para un servicio en un país. Se cobra al momento; se reembolsa si no llega el código. Respuesta: ACCESS_NUMBER:número_de_pedido:teléfono.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `service` | sí | Código del servicio (getServicesList), p. ej. tg. |
| `country` | sí | Código del país (getCountries). |
| `maxPrice` | no | Precio máximo en dólares. Si es más caro: WRONG_MAX_PRICE con el precio actual, sin cobro. |

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

Respuesta:

```
ACCESS_NUMBER:1042:15550001111
```

Errores posibles: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`, `BANNED`

### `getNumberV2`

Igual que getNumber, con respuesta JSON que incluye el precio y los tiempos de la ventana.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `service` | sí | Código del servicio. |
| `country` | sí | Código del país. |
| `maxPrice` | no | Precio máximo en dólares. |

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

Respuesta:

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

Errores posibles: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`

### `getStatus`

Estado del pedido: esperando código, código recibido (con el código) o cancelado.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `id` | sí | Número de pedido de getNumber. |

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

Respuesta:

```
STATUS_OK:48213
```

Errores posibles: `NO_ACTIVATION`

### `setStatus`

Actuar sobre un pedido: listo, otro código, terminar, cancelar (ver Estados).

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `id` | sí | Número de pedido. |
| `status` | sí | 1, 3, 6 u 8. |

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

Respuesta:

```
ACCESS_ACTIVATION
```

Errores posibles: `NO_ACTIVATION`, `BAD_STATUS`, `EARLY_CANCEL_DENIED`, `ERROR_SQL`

### `getActiveActivations`

Todos los pedidos abiertos de la clave: número, estado, códigos y textos recibidos.

Solo api_key y action.

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

Respuesta:

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

Nuestros precios en dólares y cuántos números hay aproximadamente. Sin parámetros: todo lo que está a la venta.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `service` | no | Solo este servicio. |
| `country` | no | Solo este país. |

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

Respuesta:

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

Errores posibles: `BAD_SERVICE`, `BAD_COUNTRY`

### `getCountries`

Países con códigos del protocolo y nombres en ruso e inglés.

Solo api_key y action.

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

Respuesta:

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

### `getServicesList`

Servicios con códigos del protocolo.

Solo api_key y action.

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

Respuesta:

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

## Estados

Valores de status en setStatus:

- `status=1` → `ACCESS_READY` — El número está introducido en el servicio; esperando código. No cambia nada; se mantiene por compatibilidad.
- `status=3` → `ACCESS_RETRY_GET` — Pedir otro código al mismo número: después del primer código y mientras el pedido siga abierto. Gratis. Hasta que llegue, getStatus responde STATUS_WAIT_RETRY.
- `status=6` → `ACCESS_ACTIVATION` — Terminar el pedido tras el código. No llegarán más SMS al número.
- `status=8` → `ACCESS_CANCEL` — Cancelar un pedido sin código (no antes de 2 minutos). El dinero vuelve a tu saldo.

Respuestas de getStatus:

- `STATUS_WAIT_CODE` — Aún no hay código: vuelve a preguntar en 5–10 segundos.
- `STATUS_OK:48213` — Llegó el código; tras los dos puntos va el código (o el texto del SMS si no lo contiene).
- `STATUS_WAIT_RETRY:48213` — Se pidió otro código y aún no llegó; tras los dos puntos va el código anterior.
- `STATUS_CANCEL` — Cancelado o cerrado sin código; el dinero ya está en tu saldo.

## Errores

Los errores llegan como texto plano, como es habitual en este protocolo. Todos, salvo RATE_LIMIT, llegan con estado 200.

- `NO_KEY` — Falta api_key.
- `BAD_KEY` — Clave incorrecta o desactivada, o cuenta bloqueada.
- `BAD_ACTION` — Acción desconocida.
- `BAD_SERVICE` — No existe ese código de servicio.
- `BAD_COUNTRY` — No existe ese código de país.
- `NO_NUMBERS` — Ahora no hay números para esa combinación; no se cobró nada.
- `NO_BALANCE` — Menos de $20 de saldo (umbral de la API) o menos que el precio.
- `WRONG_MAX_PRICE` — El precio supera maxPrice; tras los dos puntos va el precio actual.
- `LIMIT_ACTIVATIONS` — Demasiados pedidos abiertos: termina o cancela algunos.
- `RATE_LIMIT` — Demasiado frecuente; HTTP 429. Reintenta en un segundo.
- `BANNED` — La cuenta está bloqueada: contacta con soporte.
- `NO_ACTIVATION` — No hay ningún pedido con ese número para esta clave.
- `BAD_STATUS` — Esa acción no está disponible ahora para el pedido (p. ej., terminar sin código).
- `EARLY_CANCEL_DENIED` — Solo se puede cancelar 2 minutos después de la compra.
- `ERROR_SQL` — Error temporal de nuestro lado: repite la solicitud en un minuto.

## Códigos de servicios y países

Los códigos son los estándar del protocolo: tg — Telegram, wa — WhatsApp, etc.; el país 187 — EE. UU. getServicesList y getCountries devuelven las listas completas: toma los códigos de ahí, no de tablas antiguas, porque servicios y países cambian con el stock.

## REST API v1

Los mismos números y las mismas reglas que handler_api.php, en JSON: servicios por nombre (telegram), países por código ISO (US), estados HTTP y códigos de error con los que un programa puede decidir. Úsala para código nuevo; deja handler_api.php para el software que ya lo usa.

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

### Clave

Envía la clave en un encabezado: Authorization: Bearer <clave> (o X-Api-Key: <clave>). Nunca en la URL: las URL acaban en los logs. Los límites, el mínimo de $20 y el catálogo son los mismos que en handler_api.php.

### Reintentos sin compras dobles

Añade el encabezado Idempotency-Key con un valor propio (un UUID, hasta 255 caracteres) en POST /orders y /orders/batch. Si se corta la conexión y repites la petición con la misma clave, recibes la primera respuesta (con el encabezado Idempotent-Replayed: true) y no se compra nada dos veces. La misma clave con otro cuerpo se rechaza; una repetición que llega mientras la primera sigue en curso recibe idempotency_in_progress. Las respuestas correctas se guardan 24 horas; una compra fallida no cobró nada y puede repetirse con la misma clave.

### Varios números a la vez

POST /orders/batch compra hasta 20 números de un servicio y país en una sola petición: es la forma de comprar más de 5 por segundo. Las compras van una tras otra; la primera que falla detiene el lote, y la respuesta trae lo comprado y el motivo en stoppedBy. Si no se compró nada, la respuesta es ese error.

### Páginas

GET /orders devuelve primero los más nuevos. Si next no es null, pásalo como before para obtener la página siguiente.

### Webhooks

En lugar de consultar GET /orders/{id}, define una dirección HTTPS con PUT /webhook y te enviaremos un POST cuando algo le pase a un pedido comprado por la API. El cuerpo trae el pedido tal como está al enviarse. Responde con cualquier 2xx en 10 segundos; un pedido puede generar varios eventos y el mismo evento puede llegar dos veces tras un reintento, así que usa el id de la entrega.

- `order.code_received` — Llegó un código (cada nuevo lo envía otra vez).
- `order.finished` — El pedido se terminó: por ti, o al acabarse su tiempo después de un código.
- `order.cancelled` — Cancelado sin código; el dinero volvió al saldo.
- `order.expired` — Se acabó el tiempo sin código; el dinero volvió al saldo.
- `ping` — Una prueba de POST /webhook/test; order es 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", … }], … }
}
```

Cada petición trae X-RelySMS-Signature: t=<hora unix>,v1=<firma>. La firma es HMAC-SHA256 de la cadena "t.cuerpo" (el cuerpo tal cual, byte a byte) con el secreto del webhook, en hex. Verifícala, y que t no tenga más de 5 minutos, antes de confiar en el cuerpo.

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

Si tu servidor no responde 2xx, reintentamos a los 10 segundos, 1 minuto, 5 minutos, 30 minutos, 2 horas y 6 horas —7 intentos en total— y luego lo dejamos. GET /webhook/deliveries muestra qué se envió y qué falló.

### Endpoints

#### `GET /balance`

El saldo en dólares y si alcanza para comprar por la API.

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

Respuesta:

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

#### `GET /services`

Todos los servicios a la venta: nuestro id, el nombre y el código de handler_api.php.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `lang` (query) | no | Idioma de los nombres: en (por defecto), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Respuesta:

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

#### `GET /countries`

Todos los países a la venta: código ISO, nombre y código de handler_api.php.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `lang` (query) | no | Idioma de los nombres: en (por defecto), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Respuesta:

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

#### `GET /prices`

Nuestros precios en dólares y cuántos números hay aproximadamente. Sin parámetros, todo lo que está a la venta.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `service` (query) | no | Id del servicio de /services, p. ej. telegram. |
| `country` (query) | no | Código ISO del país de /countries, p. ej. US. |

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

Respuesta:

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

Errores posibles: `unknown_service`, `unknown_country`

#### `POST /orders`

Comprar un número. Se cobra al instante y se devuelve si no llega el código.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `service` (body) | sí | Id del servicio de /services, p. ej. telegram. |
| `country` (body) | sí | Código ISO del país de /countries, p. ej. US. |
| `maxPrice` (body) | no | Precio máximo en dólares, un número. Por encima: price_above_max con el precio actual, sin cobro. |

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

Respuesta:

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

Errores posibles: `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`

Comprar hasta 20 números de un servicio y país en una sola petición.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `service` (body) | sí | Id del servicio de /services, p. ej. telegram. |
| `country` (body) | sí | Código ISO del país de /countries, p. ej. US. |
| `quantity` (body) | sí | Cuántos números, de 1 a 20. |
| `maxPrice` (body) | no | Precio máximo en dólares, un número. Por encima: price_above_max con el precio actual, sin cobro. |

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

Respuesta:

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

Errores posibles: `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`

Tus pedidos, los más nuevos primero: los abiertos por defecto, o todos.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `status` (query) | no | open (por defecto): esperando o con código; all: todos los pedidos. |
| `limit` (query) | no | Pedidos por página, de 1 a 100 (por defecto 50). |
| `before` (query) | no | El valor next de la página anterior. |

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

Respuesta:

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

Errores posibles: `bad_request`

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

Un pedido con sus códigos. Consulta cada 5–10 segundos mientras el estado sea waiting.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `id` (path) | sí | El id del pedido de la respuesta de compra. |

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

Respuesta:

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

Errores posibles: `not_found`

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

Terminar un pedido con código. El número no recibirá más SMS. Repetirlo no hace daño.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `id` (path) | sí | El id del pedido de la respuesta de compra. |

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

Respuesta:

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

Errores posibles: `not_found`, `invalid_state`

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

Otro código al mismo número: después del primer código y mientras el pedido siga abierto. Gratis. Hasta que llegue, el pedido tiene awaitingAnotherCode = true.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `id` (path) | sí | El id del pedido de la respuesta de compra. |

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

Respuesta:

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

Errores posibles: `not_found`, `invalid_state`, `try_later`

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

Cancelar un pedido sin código, a partir de 2 minutos tras la compra. El dinero vuelve al instante.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `id` (path) | sí | El id del pedido de la respuesta de compra. |

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

Respuesta:

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

Errores posibles: `not_found`, `invalid_state`, `too_early_to_cancel`

#### `GET /webhook`

La dirección del webhook y su secreto de firma, o null.

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

Respuesta:

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

#### `PUT /webhook`

Definir o cambiar la dirección. El secreto se crea una vez y se conserva; rotateSecret crea uno nuevo.

| Parámetro | Obligatorio | Descripción |
|---|---|---|
| `url` (body) | sí | Dirección HTTPS de tu servidor. Se rechazan direcciones privadas y locales. |
| `rotateSecret` (body) | no | true: crear un nuevo secreto de firma. |

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

Respuesta:

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

Errores posibles: `bad_request`, `invalid_webhook_url`

#### `DELETE /webhook`

Quitar el webhook. Los eventos aún no enviados se descartan.

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

Respuesta:

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

#### `POST /webhook/test`

Enviar un evento ping para comprobar la dirección y la firma.

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

Respuesta:

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

Errores posibles: `not_found`

#### `GET /webhook/deliveries`

Las últimas 20 entregas: evento, estado, intentos, última respuesta de tu servidor.

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

Respuesta:

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

### Estados del pedido

El campo status de un pedido:

- `waiting` — El número es tuyo y aún no llegó ningún código.
- `received` — Llegó un código; está en codes. Pueden llegar más mientras el pedido siga abierto.
- `finished` — Terminaste el pedido (o se acabó su tiempo después de un código).
- `cancelled` — Cancelado sin código; refunded es true y el dinero está en el saldo.
- `expired` — Se acabó el tiempo sin código; el dinero volvió al saldo.

### Errores

Un error es un estado HTTP y un JSON con code para que el programa decida y message para las personas. Algunos traen campos extra (price, cancelableAt, apiMinimum).

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

- `missing_key` (HTTP 401) — No hay clave en Authorization ni en X-Api-Key.
- `invalid_key` (HTTP 401) — La clave es incorrecta, fue reemplazada o la cuenta está bloqueada.
- `bad_request` (HTTP 400) — El cuerpo o un parámetro es incorrecto; message dice cuál.
- `unknown_service` (HTTP 400) — No existe ese id de servicio.
- `unknown_country` (HTTP 400) — No existe ese código de país.
- `idempotency_key_invalid` (HTTP 400) — Idempotency-Key está vacío o supera los 255 caracteres.
- `invalid_webhook_url` (HTTP 400) — La dirección del webhook no es HTTPS, no es una URL o apunta a un servidor privado o local.
- `insufficient_balance` (HTTP 402) — El saldo es menor que el precio.
- `below_api_minimum` (HTTP 402) — Menos de $20 de saldo: la API solo compra desde $20.
- `account_blocked` (HTTP 403) — La cuenta está bloqueada: escribe a soporte.
- `not_found` (HTTP 404) — No hay pedido con ese id para esta clave.
- `no_numbers` (HTTP 409) — No hay números para este par ahora; no se cobró nada.
- `price_above_max` (HTTP 409) — El precio supera maxPrice; price trae el actual.
- `too_many_open_orders` (HTTP 409) — Demasiados pedidos abiertos: termina o cancela algunos.
- `invalid_state` (HTTP 409) — No es posible en este estado (p. ej. terminar sin código).
- `too_early_to_cancel` (HTTP 409) — Se puede cancelar desde cancelableAt, 2 minutos tras la compra.
- `idempotency_in_progress` (HTTP 409) — Una petición con este Idempotency-Key sigue en curso; repite en un momento.
- `idempotency_key_reused` (HTTP 422) — Este Idempotency-Key ya se usó para otra petición.
- `rate_limited` (HTTP 429) — Más de 20 peticiones por segundo; Retry-After indica cuándo repetir.
- `buying_too_fast` (HTTP 429) — Más de 5 compras por segundo; usa /orders/batch.
- `try_later` (HTTP 503) — No funcionó por ahora; inténtalo en un minuto.

### Ejemplos REST

Comprar un número, esperar el código, terminar el pedido.

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

## Ejemplos

El ciclo completo: comprar un número, esperar el código y terminar el pedido.

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

## Preguntas

**Mi software usaba otro servicio de activaciones. ¿Qué cambio?**

La dirección del servidor, a https://relysms.pro/stubs/handler_api.php; la clave, por la de tu cuenta. Los códigos de servicios y países no cambian.

**¿Por qué NO_BALANCE si tengo saldo?**

Las compras por API requieren $20 o más de saldo. La cuenta y la Mini App no tienen ese umbral.

**¿Qué pasa con el dinero si no llega el código?**

Vuelve solo a tu saldo cuando se cierra la ventana, o al momento si cancelas a los 2 minutos.

**¿Puedo recibir un segundo código en el mismo número?**

Sí, mientras la ventana esté abierta: setStatus=3 y luego getStatus otra vez.

**¿Hay modo de pruebas?**

No: todas las compras son reales. Empieza con una combinación barata y prueba tu flujo con un número.

**Se filtró mi clave. ¿Qué hago?**

Cuenta → Saldo → API → Emitir una nueva. La anterior deja de funcionar al instante.
