# RelySMS — API de réception de SMS — pour votre logiciel

Si votre logiciel fonctionne déjà avec un service d'activation via handler_api.php, il fonctionne avec RelySMS : changez l'adresse du serveur et la clé. Mêmes actions, mêmes réponses, nos prix et nos remboursements.

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

## Démarrage rapide

1. Connectez-vous et ouvrez Solde → API → Créer une clé. La clé n'est affichée qu'une fois — conservez-la.
2. Rechargez jusqu'à 20 $ ou plus : les achats par l'API ne passent qu'avec ce solde. Garder une clé avec un solde nul ne pose aucun problème.
3. Indiquez dans votre logiciel le serveur https://relysms.pro/stubs/handler_api.php et votre clé à la place de celle du service précédent.
4. Achetez un numéro (getNumber), puis interrogez le statut toutes les 5 à 10 secondes (getStatus) jusqu'à recevoir STATUS_OK avec le code.
5. Terminez la commande (setStatus=6). Sans code, annulez après 2 minutes (setStatus=8) ou laissez la fenêtre se terminer : l'argent revient tout seul.

## Règles et limites

- Les achats par l'API exigent un solde d'au moins 20 $. En dessous, getNumber répond NO_BALANCE ; les autres méthodes fonctionnent.
- Le numéro est à vous pendant 20 minutes. Sans code, la commande se ferme d'elle-même et l'argent revient sur votre solde.
- Vous pouvez annuler 2 minutes après l'achat si aucun code n'est arrivé. L'argent revient aussitôt.
- Dès le premier code, la commande est payée et non remboursable. Tant que la fenêtre est ouverte, d'autres codes peuvent encore arriver.
- Jusqu'à 20 requêtes et 5 achats par seconde et par clé ; jusqu'à 50 commandes ouvertes à la fois.
- Le prix est fixé à l'achat. maxPrice vous évite d'acheter au-dessus de votre limite.
- Les conditions du service s'appliquent : spam, fraude et inscriptions en masse à ces fins sont interdits.

## Méthodes

Chaque requête est un GET ou un POST vers une seule adresse, avec les paramètres dans l'URL ou dans un formulaire. La clé va dans api_key, l'action dans action.

### `getBalance`

Le solde du compte en dollars.

Seulement api_key et action.

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

Réponse:

```
ACCESS_BALANCE:25.00
```

### `getNumber`

Acheter un numéro pour un service dans un pays. Débité tout de suite ; remboursé si aucun code n'arrive. Réponse : ACCESS_NUMBER:numéro_de_commande:téléphone.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `service` | oui | Code du service (getServicesList), p. ex. tg. |
| `country` | oui | Code du pays (getCountries). |
| `maxPrice` | non | Prix maximal en dollars. Au-delà : WRONG_MAX_PRICE avec le prix actuel, rien n'est débité. |

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

Réponse:

```
ACCESS_NUMBER:1042:15550001111
```

Erreurs possibles: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`, `BANNED`

### `getNumberV2`

Comme getNumber, avec une réponse JSON incluant le prix et les horaires de la fenêtre.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `service` | oui | Code du service. |
| `country` | oui | Code du pays. |
| `maxPrice` | non | Prix maximal en dollars. |

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

Réponse:

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

Erreurs possibles: `BAD_SERVICE`, `BAD_COUNTRY`, `NO_NUMBERS`, `NO_BALANCE`, `WRONG_MAX_PRICE:0.59`, `LIMIT_ACTIVATIONS`, `RATE_LIMIT`

### `getStatus`

Statut de la commande : en attente du code, code reçu (avec le code) ou annulée.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `id` | oui | Numéro de commande renvoyé par getNumber. |

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

Réponse:

```
STATUS_OK:48213
```

Erreurs possibles: `NO_ACTIVATION`

### `setStatus`

Agir sur une commande : prêt, autre code, terminer, annuler (voir Statuts).

| Paramètre | Obligatoire | Description |
|---|---|---|
| `id` | oui | Numéro de commande. |
| `status` | oui | 1, 3, 6 ou 8. |

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

Réponse:

```
ACCESS_ACTIVATION
```

Erreurs possibles: `NO_ACTIVATION`, `BAD_STATUS`, `EARLY_CANCEL_DENIED`, `ERROR_SQL`

### `getActiveActivations`

Toutes les commandes ouvertes de la clé : numéro, statut, codes et textes reçus.

Seulement api_key et action.

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

Réponse:

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

Nos prix en dollars et le nombre approximatif de numéros. Sans paramètres : tout ce qui est en vente.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `service` | non | Seulement ce service. |
| `country` | non | Seulement ce pays. |

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

Réponse:

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

Erreurs possibles: `BAD_SERVICE`, `BAD_COUNTRY`

### `getCountries`

Liste des pays avec les codes du protocole et les noms en russe et en anglais.

Seulement api_key et action.

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

Réponse:

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

### `getServicesList`

Liste des services avec les codes du protocole.

Seulement api_key et action.

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

Réponse:

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

## Statuts

Valeurs de status dans setStatus :

- `status=1` → `ACCESS_READY` — Le numéro est saisi dans le service, en attente du code. Ne change rien ; conservé pour la compatibilité.
- `status=3` → `ACCESS_RETRY_GET` — Demander un autre code sur le même numéro — après le premier code, tant que la commande est ouverte. Gratuit. En attendant, getStatus répond STATUS_WAIT_RETRY.
- `status=6` → `ACCESS_ACTIVATION` — Terminer la commande après le code. Aucun nouveau SMS n'arrivera sur le numéro.
- `status=8` → `ACCESS_CANCEL` — Annuler une commande sans code (pas avant 2 minutes). L'argent revient sur votre solde.

Réponses de getStatus :

- `STATUS_WAIT_CODE` — Pas encore de code — redemandez dans 5 à 10 secondes.
- `STATUS_OK:48213` — Le code est arrivé ; après les deux-points figure le code (ou le texte du SMS s'il n'en contient pas).
- `STATUS_WAIT_RETRY:48213` — Un autre code a été demandé et n'est pas encore arrivé ; après les deux-points figure le code précédent.
- `STATUS_CANCEL` — Annulée ou close sans code ; l'argent est déjà revenu sur le solde.

## Erreurs

Les erreurs sont des chaînes de texte, comme le veut l'usage de ce protocole. Toutes, sauf RATE_LIMIT, arrivent avec le statut 200.

- `NO_KEY` — api_key manquant.
- `BAD_KEY` — Clé erronée ou désactivée, ou compte bloqué.
- `BAD_ACTION` — Action inconnue.
- `BAD_SERVICE` — Ce code de service n'existe pas.
- `BAD_COUNTRY` — Ce code de pays n'existe pas.
- `NO_NUMBERS` — Pas de numéro pour cette paire pour l'instant ; rien n'est débité.
- `NO_BALANCE` — Moins de 20 $ de solde (seuil de l'API) ou moins que le prix.
- `WRONG_MAX_PRICE` — Le prix dépasse maxPrice ; le prix actuel suit les deux-points.
- `LIMIT_ACTIVATIONS` — Trop de commandes ouvertes — terminez-en ou annulez-en.
- `RATE_LIMIT` — Trop fréquent ; HTTP 429. Réessayez dans une seconde.
- `BANNED` — Le compte est bloqué — contactez le support.
- `NO_ACTIVATION` — Aucune commande avec ce numéro pour cette clé.
- `BAD_STATUS` — Cette action n'est pas disponible pour la commande en ce moment (p. ex. terminer sans code).
- `EARLY_CANCEL_DENIED` — L'annulation n'est possible que 2 minutes après l'achat.
- `ERROR_SQL` — Erreur temporaire de notre côté — répétez la requête dans une minute.

## Codes des services et pays

Les codes sont ceux, standard, du protocole : tg — Telegram, wa — WhatsApp, etc. ; le pays 187 — les États-Unis. getServicesList et getCountries renvoient les listes complètes — prenez les codes là, pas dans d'anciens tableaux : services et pays évoluent avec le stock.

## API REST v1

Les mêmes numéros et les mêmes règles que handler_api.php, en JSON : services par nom (telegram), pays par code ISO (US), statuts HTTP et codes d'erreur sur lesquels un programme peut s'appuyer. Utilisez-la pour du code neuf ; gardez handler_api.php pour les logiciels qui le parlent déjà.

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

### Clé

Envoyez la clé dans un en-tête : Authorization: Bearer <clé> (ou X-Api-Key: <clé>). Jamais dans l'URL — les URL finissent dans les logs. Les limites, le seuil de 20 $ et le catalogue sont ceux de handler_api.php.

### Réessayer sans acheter deux fois

Ajoutez l'en-tête Idempotency-Key avec une valeur à vous (un UUID, jusqu'à 255 caractères) sur POST /orders et /orders/batch. Si la connexion coupe et que vous renvoyez la requête avec la même clé, vous recevez la première réponse (avec l'en-tête Idempotent-Replayed: true) et rien n'est acheté deux fois. La même clé avec un autre corps est refusée ; une répétition arrivée pendant que la première tourne encore reçoit idempotency_in_progress. Les réponses réussies sont gardées 24 heures ; un achat échoué n'a rien débité et peut être relancé avec la même clé.

### Plusieurs numéros d'un coup

POST /orders/batch achète jusqu'à 20 numéros pour un service et un pays en une requête — c'est ainsi qu'on achète plus de 5 par seconde. Les achats se font un par un ; le premier qui échoue arrête le lot, et la réponse contient ce qui a été acheté et la raison dans stoppedBy. Si rien n'a été acheté, la réponse est cette erreur.

### Pages

GET /orders renvoie d'abord les plus récentes. Quand next n'est pas null, passez-le dans before pour obtenir la page suivante.

### Webhooks

Plutôt que d'interroger GET /orders/{id}, définissez une adresse HTTPS avec PUT /webhook : nous y envoyons un POST quand il arrive quelque chose à une commande passée par l'API. Le corps contient la commande telle qu'elle est au moment de l'envoi. Répondez par n'importe quel 2xx sous 10 secondes ; une commande peut produire plusieurs événements, et le même événement peut arriver deux fois après une nouvelle tentative : appuyez-vous sur l'id de livraison.

- `order.code_received` — Un code est arrivé (chaque nouveau code le renvoie).
- `order.finished` — La commande est terminée — par vous, ou à la fin de son temps après un code.
- `order.cancelled` — Annulée sans code ; l'argent est revenu sur le solde.
- `order.expired` — Temps écoulé sans code ; l'argent est revenu sur le solde.
- `ping` — Un test depuis POST /webhook/test ; order vaut 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", … }], … }
}
```

Chaque requête porte X-RelySMS-Signature: t=<heure unix>,v1=<signature>. La signature est le HMAC-SHA256 de la chaîne « t.corps » (le corps brut, octet pour octet) avec le secret du webhook, en hexadécimal. Vérifiez-la, ainsi que t date de moins de 5 minutes, avant de faire confiance au corps.

```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 votre serveur ne répond pas 2xx, nous réessayons après 10 secondes, 1 minute, 5 minutes, 30 minutes, 2 heures et 6 heures — 7 tentatives en tout — puis abandonnons. GET /webhook/deliveries montre ce qui est parti et ce qui a échoué.

### Points d'accès

#### `GET /balance`

Le solde en dollars, et s'il suffit pour acheter par l'API.

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

Réponse:

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

#### `GET /services`

Tous les services en vente : notre id, le nom et le code handler_api.php.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `lang` (query) | non | Langue des noms : en (par défaut), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Réponse:

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

#### `GET /countries`

Tous les pays en vente : code ISO, nom et code handler_api.php.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `lang` (query) | non | Langue des noms : en (par défaut), ru, uk, zh, es, pt, id, vi, tr, ar, fr. |

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

Réponse:

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

#### `GET /prices`

Nos prix en dollars et le nombre approximatif de numéros. Sans paramètres : tout ce qui est en vente.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `service` (query) | non | Id du service tiré de /services, p. ex. telegram. |
| `country` (query) | non | Code ISO du pays tiré de /countries, p. ex. US. |

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

Réponse:

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

Erreurs possibles: `unknown_service`, `unknown_country`

#### `POST /orders`

Acheter un numéro. Débité tout de suite ; remboursé si aucun code n'arrive.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `service` (body) | oui | Id du service tiré de /services, p. ex. telegram. |
| `country` (body) | oui | Code ISO du pays tiré de /countries, p. ex. US. |
| `maxPrice` (body) | non | Prix maximal en dollars, un nombre. Au-delà : price_above_max avec le prix actuel, rien n'est débité. |

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

Réponse:

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

Erreurs possibles: `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`

Acheter jusqu'à 20 numéros pour un service et un pays en une requête.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `service` (body) | oui | Id du service tiré de /services, p. ex. telegram. |
| `country` (body) | oui | Code ISO du pays tiré de /countries, p. ex. US. |
| `quantity` (body) | oui | Combien de numéros, de 1 à 20. |
| `maxPrice` (body) | non | Prix maximal en dollars, un nombre. Au-delà : price_above_max avec le prix actuel, rien n'est débité. |

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

Réponse:

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

Erreurs possibles: `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`

Vos commandes, les plus récentes d'abord : les ouvertes par défaut, ou toutes.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `status` (query) | non | open (par défaut) — en attente ou avec code ; all — toutes les commandes. |
| `limit` (query) | non | Commandes par page, de 1 à 100 (50 par défaut). |
| `before` (query) | non | La valeur next de la page précédente. |

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

Réponse:

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

Erreurs possibles: `bad_request`

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

Une commande avec ses codes. Interrogez-la toutes les 5 à 10 secondes tant que le statut est waiting.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `id` (path) | oui | L'id de la commande renvoyé à l'achat. |

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

Réponse:

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

Erreurs possibles: `not_found`

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

Terminer une commande qui a un code. Le numéro ne recevra plus de SMS. Recommencer est sans effet.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `id` (path) | oui | L'id de la commande renvoyé à l'achat. |

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

Réponse:

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

Erreurs possibles: `not_found`, `invalid_state`

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

Un autre code sur le même numéro — après le premier code, tant que la commande est ouverte. Gratuit. En attendant, la commande a awaitingAnotherCode = true.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `id` (path) | oui | L'id de la commande renvoyé à l'achat. |

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

Réponse:

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

Erreurs possibles: `not_found`, `invalid_state`, `try_later`

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

Annuler une commande sans code, 2 minutes après l'achat. L'argent revient aussitôt.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `id` (path) | oui | L'id de la commande renvoyé à l'achat. |

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

Réponse:

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

Erreurs possibles: `not_found`, `invalid_state`, `too_early_to_cancel`

#### `GET /webhook`

L'adresse du webhook et son secret de signature, ou null.

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

Réponse:

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

#### `PUT /webhook`

Définir ou changer l'adresse. Le secret est créé une fois et conservé ; rotateSecret en crée un nouveau.

| Paramètre | Obligatoire | Description |
|---|---|---|
| `url` (body) | oui | Adresse HTTPS de votre serveur. Les adresses privées et locales sont refusées. |
| `rotateSecret` (body) | non | true — créer un nouveau secret de signature. |

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

Réponse:

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

Erreurs possibles: `bad_request`, `invalid_webhook_url`

#### `DELETE /webhook`

Supprimer le webhook. Les événements pas encore envoyés sont abandonnés.

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

Réponse:

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

#### `POST /webhook/test`

Envoyer un événement ping pour vérifier l'adresse et la signature.

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

Réponse:

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

Erreurs possibles: `not_found`

#### `GET /webhook/deliveries`

Les 20 dernières livraisons : événement, statut, tentatives, dernière réponse de votre serveur.

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

Réponse:

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

### Statuts de commande

Le champ status d'une commande :

- `waiting` — Le numéro est à vous et aucun code n'est encore arrivé.
- `received` — Un code est arrivé ; il est dans codes. D'autres peuvent arriver tant que la commande est ouverte.
- `finished` — Vous avez terminé la commande (ou son temps s'est écoulé après un code).
- `cancelled` — Annulée sans code ; refunded vaut true et l'argent est sur le solde.
- `expired` — Le temps s'est écoulé sans code ; l'argent est revenu sur le solde.

### Erreurs

Une erreur, c'est un statut HTTP et un JSON avec code pour le programme et message pour les humains. Certaines portent des champs en plus (price, cancelableAt, apiMinimum).

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

- `missing_key` (HTTP 401) — Aucune clé dans Authorization ni dans X-Api-Key.
- `invalid_key` (HTTP 401) — La clé est fausse, a été remplacée, ou le compte est bloqué.
- `bad_request` (HTTP 400) — Le corps ou un paramètre est incorrect ; message précise lequel.
- `unknown_service` (HTTP 400) — Cet id de service n'existe pas.
- `unknown_country` (HTTP 400) — Ce code de pays n'existe pas.
- `idempotency_key_invalid` (HTTP 400) — Idempotency-Key est vide ou dépasse 255 caractères.
- `invalid_webhook_url` (HTTP 400) — L'adresse du webhook n'est pas en HTTPS, n'est pas une URL ou pointe vers un serveur privé ou local.
- `insufficient_balance` (HTTP 402) — Le solde est inférieur au prix.
- `below_api_minimum` (HTTP 402) — Moins de 20 $ de solde — l'API n'achète qu'à partir de 20 $.
- `account_blocked` (HTTP 403) — Le compte est bloqué — contactez le support.
- `not_found` (HTTP 404) — Aucune commande avec cet id pour cette clé.
- `no_numbers` (HTTP 409) — Pas de numéro pour cette paire pour l'instant ; rien n'est débité.
- `price_above_max` (HTTP 409) — Le prix dépasse maxPrice ; price contient le prix actuel.
- `too_many_open_orders` (HTTP 409) — Trop de commandes ouvertes — terminez-en ou annulez-en.
- `invalid_state` (HTTP 409) — Impossible dans ce statut (p. ex. terminer sans code).
- `too_early_to_cancel` (HTTP 409) — L'annulation est possible à partir de cancelableAt, 2 minutes après l'achat.
- `idempotency_in_progress` (HTTP 409) — Une requête avec cet Idempotency-Key est encore en cours ; réessayez dans un instant.
- `idempotency_key_reused` (HTTP 422) — Cet Idempotency-Key a déjà servi pour une autre requête.
- `rate_limited` (HTTP 429) — Plus de 20 requêtes par seconde ; Retry-After indique quand réessayer.
- `buying_too_fast` (HTTP 429) — Plus de 5 achats par seconde ; utilisez /orders/batch.
- `try_later` (HTTP 503) — Échec temporaire — réessayez dans une minute.

### Exemples REST

Acheter un numéro, attendre le code, terminer la commande.

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

## Exemples

Le cycle complet : acheter un numéro, attendre le code, terminer la commande.

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

## Questions

**Mon logiciel utilisait un autre service d'activation. Que dois-je changer ?**

L'adresse du serveur en https://relysms.pro/stubs/handler_api.php et la clé par celle de votre compte. Les codes des services et des pays restent les mêmes.

**Pourquoi NO_BALANCE alors que j'ai du solde ?**

Les achats par l'API exigent un solde de 20 $ ou plus. Le compte et la Mini App n'ont pas ce seuil.

**Et l'argent, si le code n'arrive pas ?**

Il revient tout seul sur votre solde à la fin de la fenêtre, ou aussitôt si vous annulez après 2 minutes.

**Puis-je recevoir un second code sur le même numéro ?**

Oui, tant que la fenêtre est ouverte : setStatus=3, puis à nouveau getStatus.

**Y a-t-il un mode test ?**

Non : tous les achats sont réels. Commencez par une paire bon marché et testez votre flux sur un numéro.

**Ma clé a fuité. Que faire ?**

Compte → Solde → API → En émettre une nouvelle. L'ancienne cesse de fonctionner aussitôt.
