<!-- Documentation de l'API revendeur Airproxy, version Markdown de https://airproxy.store/api-docs (générée le 2026-10-10). -->

# API revendeur

Achetez des proxies avec votre solde depuis votre propre programme, à toute heure et sans passer par le site. Les identifiants reviennent dans la réponse, et les proxies apparaissent aussitôt dans votre espace revendeur.

- **URL de base** : `https://airproxy.store/api/v1/reseller`
- **Authentification** : `Authorization: Bearer VOTRE_CLE_API`
- **Format** : JSON en UTF-8, montants en euros, dates ISO 8601 en UTC
- **Version** : `v1` (prévoyez que des champs puissent être ajoutés aux réponses)

## Vue d'ensemble

L'API automatise ce que vous faites à la main depuis la page d'achat : lire votre solde, consulter le stock et le prix d'une commande, puis acheter des proxies ISP pour la France, l'Espagne ou l'Europe. Chaque proxy est valable 30 jours.

Un achat par API se comporte exactement comme un achat au solde sur le site :

- le montant est débité de votre solde, avec votre remise de palier ;
- une facture est émise, comme pour tout achat ;
- les proxies sont créés sur votre compte et leurs accès vous sont renvoyés dans la réponse ;
- ils sont ajoutés à la page Revente de votre espace revendeur, avec un prix de vente à 0 que vous renseignez ensuite à la main (ou que vous passez dans la requête).

| Méthode | Route | Rôle |
| --- | --- | --- |
| GET | `/balance` | Votre solde actuel. |
| GET | `/stock` | Disponibilité et prix d'une commande de N proxies, pays par pays. |
| POST | `/buy` | Achat de proxies : débit du solde et livraison immédiate des accès. |

> **Ce que l'API ne fait pas.** Le renouvellement et le reset d'un proxy ne passent pas par l'API : ils se font depuis votre espace client.

## Démarrage rapide

1. **Générez votre clé.** Dans votre espace client, ouvrez l'onglet « Espace revendeur », dépliez le bloc « API d'achat automatique » et cliquez sur « Générer une clé ». Copiez-la tout de suite : elle n'est affichée qu'une seule fois.
2. **Vérifiez qu'elle fonctionne** en lisant votre solde.
3. **Passez une première commande.**

Lecture du solde :

```bash
export AIRPROXY_KEY="apr_votre_cle_ici"

curl https://airproxy.store/api/v1/reseller/balance \
  -H "Authorization: Bearer $AIRPROXY_KEY"
```

```json
{ "ok": true, "balance": 94.3, "currency": "EUR" }
```

Achat de 2 proxies français :

```bash
curl -X POST https://airproxy.store/api/v1/reseller/buy \
  -H "Authorization: Bearer $AIRPROXY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"country":"fr","qty":2}'
```

```json
{
  "ok": true,
  "order_id": 27,
  "country": "fr",
  "qty": 2,
  "total_debited": 5.4,
  "proxies": [
    {
      "proxy_id": "1791018004491.359791309",
      "host": "203.0.113.10",
      "port": 8888,
      "username": "airproxy4821",
      "password": "k3QxT9mW2vLp8dRa",
      "access": "203.0.113.10:8888:airproxy4821:k3QxT9mW2vLp8dRa",
      "country": "fr",
      "expires_at": "2026-11-02T09:00:04.491Z"
    },
    {
      "proxy_id": "1791018004492.928588261",
      "host": "203.0.113.10",
      "port": 8888,
      "username": "airproxy4822",
      "password": "8JZ5f1O7xG6jPrt5",
      "access": "203.0.113.10:8888:airproxy4822:8JZ5f1O7xG6jPrt5",
      "country": "fr",
      "expires_at": "2026-11-02T09:00:04.491Z"
    }
  ],
  "discount": 0.6,
  "balance": 88.9,
  "listed_in_resale": true
}
```

> ⚠️ **Il n'existe pas d'environnement de test.** Tout achat est réel et débité de votre solde. Pour vérifier votre intégration, commandez 1 seul proxy.

Sous Windows, remplacez `$(uuidgen)` par n'importe quelle chaîne unique de 8 caractères ou plus. Les adresses des exemples (`203.0.113.10`) sont fictives : utilisez toujours le `host` et le `port` de la réponse.

## Authentification

Chaque requête porte votre clé API dans l'en-tête `Authorization`. L'en-tête `X-API-Key` est accepté en remplacement.

```bash
Authorization: Bearer apr_votre_cle_ici
```

- Il y a une seule clé par compte. En générer une nouvelle invalide l'ancienne immédiatement.
- Nous ne conservons pas la clé en clair : si vous la perdez, régénérez-en une.
- Elle permet d'acheter avec votre solde et rien d'autre. Elle ne donne accès ni à votre mot de passe, ni à vos factures, ni à vos moyens de paiement.
- Vous pouvez la révoquer à tout moment depuis le même bloc de votre espace revendeur.
- Elle cesse de fonctionner (`403 not_reseller`) si le mode revendeur est retiré de votre compte.

> ⚠️ **Gardez la clé côté serveur** : variable d'environnement ou gestionnaire de secrets, jamais dans une page web, une application mobile ou un dépôt public. L'API refuse d'ailleurs les appels depuis un navigateur.
>
> Une clé volée permet d'acheter avec votre solde, dans la limite des plafonds décrits plus bas. Si elle a fuité, régénérez-la aussitôt, et ne laissez sur le compte que le solde dont vous avez besoin.

## Conventions

- Toutes les routes sont en HTTPS. Les requêtes avec un corps envoient `Content-Type: application/json`.
- Une réponse réussie contient `"ok": true`. Une erreur renvoie `{"ok": false, "error": "...", "code": "..."}` avec le statut HTTP correspondant et, selon le cas, des champs en plus (`balance`, `available`...). Basez votre logique sur `code`, qui est stable, jamais sur `error`, qui est un texte pour les humains.
- Les montants sont des nombres en euros, avec deux décimales au plus. Les dates sont au format ISO 8601, en UTC.
- Les types sont stricts : un nombre se passe en nombre JSON (`"qty": 2`, pas `"qty": "2"`). Une valeur ambiguë est refusée plutôt qu'interprétée, parce que cette API débite.
- Les champs inconnus d'une requête sont ignorés. Faites de même avec les réponses : des champs peuvent y être ajoutés.
- Les réponses ne sont jamais mises en cache (`Cache-Control: no-store`) : elles contiennent des identifiants.
- L'API est faite pour être appelée depuis votre serveur : les appels depuis un navigateur sont bloqués.

## Routes

Les chemins ci-dessous sont relatifs à l'URL de base `https://airproxy.store/api/v1/reseller`. Les exemples lisent votre clé dans la variable `AIRPROXY_KEY`.

### GET `/balance`

Renvoie votre solde actuel.

**cURL**

```bash
curl https://airproxy.store/api/v1/reseller/balance \
  -H "Authorization: Bearer $AIRPROXY_KEY"
```

**Python**

```python
import os
import requests

r = requests.get(
    "https://airproxy.store/api/v1/reseller/balance",
    headers={"Authorization": f"Bearer {os.environ['AIRPROXY_KEY']}"},
    timeout=15,
)
print(r.json())
```

**Node.js**

```js
const res = await fetch("https://airproxy.store/api/v1/reseller/balance", {
  headers: { Authorization: `Bearer ${process.env.AIRPROXY_KEY}` },
});
console.log(await res.json());
```

#### Réponse 200

| Champ | Description |
| --- | --- |
| `ok` _boolean_ | `true`. |
| `balance` _number_ | Votre solde, en euros. |
| `currency` _string_ | Toujours `EUR`. |

```json
{ "ok": true, "balance": 94.3, "currency": "EUR" }
```

### GET `/stock`

Renvoie, pour chaque pays, le nombre de proxies disponibles et le prix d'une commande de `qty` proxies. À consulter avant un gros achat : le tarif est dégressif, donc le prix unitaire dépend de la quantité.

#### Paramètre de requête

| Nom | Description |
| --- | --- |
| `qty` _integer_ _optionnel_ | Nombre de proxies pour lequel calculer le prix, de 1 à 50. Absent : 1. Une valeur hors de cet intervalle est ramenée à la limite la plus proche. |

**cURL**

```bash
curl "https://airproxy.store/api/v1/reseller/stock?qty=5" \
  -H "Authorization: Bearer $AIRPROXY_KEY"
```

**Python**

```python
import os
import requests

r = requests.get(
    "https://airproxy.store/api/v1/reseller/stock",
    params={"qty": 5},
    headers={"Authorization": f"Bearer {os.environ['AIRPROXY_KEY']}"},
    timeout=15,
)
print(r.json())
```

**Node.js**

```js
const res = await fetch("https://airproxy.store/api/v1/reseller/stock?qty=5", {
  headers: { Authorization: `Bearer ${process.env.AIRPROXY_KEY}` },
});
console.log(await res.json());
```

#### Réponse 200

| Champ | Description |
| --- | --- |
| `ok` _boolean_ | `true`. |
| `currency` _string_ | Toujours `EUR`. |
| `stock` _array_ | Une entrée par pays proposé : `fr`, `es`, `eu`. |
| `stock[].country` _string_ | Code du pays, à reprendre tel quel dans `POST /buy`. |
| `stock[].available` _integer_ | Nombre de proxies disponibles pour ce pays. Si ce nombre est inférieur à `qty`, l'achat échoue en `out_of_stock`. |
| `stock[].qty` _integer_ | La quantité utilisée pour le calcul (celle de la requête, ramenée entre 1 et 50). |
| `stock[].total` _number_ | Montant que `POST /buy` débiterait à cet instant pour `qty` proxies de ce pays, votre remise comprise. |
| `stock[].unit_price` _number_ | Prix par proxy pour cette quantité (`total` divisé par `qty`). |

```json
{
  "ok": true,
  "currency": "EUR",
  "stock": [
    { "country": "fr", "available": 24, "qty": 5, "total": 12.6, "unit_price": 2.52 },
    { "country": "es", "available": 12, "qty": 5, "total": 9.5,  "unit_price": 1.9 },
    { "country": "eu", "available": 0,  "qty": 5, "total": 9.5,  "unit_price": 1.9 }
  ]
}
```

Les valeurs de cet exemple sont fictives. Le stock est commun à tous les clients : `available` peut changer d'un appel à l'autre.

### POST `/buy`

Achète `qty` proxies d'un pays, pour 30 jours, en débitant votre solde. La commande est tout ou rien : si le solde ou le stock ne suffit pas, rien n'est débité.

#### En-têtes

| En-tête | Description |
| --- | --- |
| `Authorization` **requis** | `Bearer VOTRE_CLE_API` |
| `Idempotency-Key` **requis** | Identifiant unique de la commande, de 8 à 128 caractères (lettres, chiffres et `. _ : -`). Voir [Idempotence](#idempotence). |
| `Content-Type` **requis** | `application/json` |

#### Corps de la requête

| Champ | Description |
| --- | --- |
| `country` _string_ **requis** | `fr`, `es` ou `eu` (la casse est ignorée). |
| `qty` _integer_ **requis** | Nombre de proxies, de 1 à 50. Un nombre JSON, pas une chaîne. |
| `price` _number_ _optionnel_ | Prix de vente à enregistrer sur la page Revente, en euros (de 0 à 10 000). Absent : 0, et vous le renseignez ensuite depuis votre espace. |

**cURL**

```bash
curl -X POST https://airproxy.store/api/v1/reseller/buy \
  -H "Authorization: Bearer $AIRPROXY_KEY" \
  -H "Idempotency-Key: commande-4821" \
  -H "Content-Type: application/json" \
  -d '{"country":"fr","qty":2}'
```

**Python**

```python
import os
import requests

r = requests.post(
    "https://airproxy.store/api/v1/reseller/buy",
    headers={
        "Authorization": f"Bearer {os.environ['AIRPROXY_KEY']}",
        "Idempotency-Key": "commande-4821",
    },
    json={"country": "fr", "qty": 2},
    timeout=30,
)
data = r.json()
if r.status_code in (200, 201):
    for p in data["proxies"]:
        print(p["access"])
else:
    print(data["code"], data["error"])
```

**Node.js**

```js
const res = await fetch("https://airproxy.store/api/v1/reseller/buy", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AIRPROXY_KEY}`,
    "Idempotency-Key": "commande-4821",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ country: "fr", qty: 2 }),
});
const data = await res.json();
if (res.ok) data.proxies.forEach((p) => console.log(p.access));
else console.error(data.code, data.error);
```

Dans ces exemples, la clé d'idempotence est dérivée de l'identifiant de commande de votre propre système, ce qui est la bonne pratique (voir [Idempotence](#idempotence)).

#### Réponse 201

La commande est créée. Un rejeu de la même requête renvoie le statut `200` avec l'en-tête `Idempotent-Replay: true`.

| Champ | Description |
| --- | --- |
| `ok` _boolean_ | `true`. |
| `order_id` _integer_ | Référence de la commande. À communiquer au support en cas de problème. |
| `country` _string_ | Pays commandé. |
| `qty` _integer_ | Nombre de proxies commandés. |
| `total_debited` _number_ | Montant débité de votre solde, en euros, remise déduite. |
| `proxies` _array_ | Les proxies livrés, décrits ci-dessous. |
| `discount` _number_ | Remise appliquée, en euros, déjà déduite de `total_debited`. Renvoyé à la création uniquement. |
| `balance` _number_ | Votre solde après l'achat. Sur un rejeu : votre solde actuel. |
| `listed_in_resale` _boolean_ | `false` si l'ajout automatique à la page Revente a échoué. Les proxies sont livrés dans tous les cas. Renvoyé à la création uniquement. |

#### Objet `proxies[]`

| Champ | Description |
| --- | --- |
| `proxy_id` _string_ | Identifiant du proxy dans votre espace. |
| `host` _string_ | Adresse du point d'entrée. |
| `port` _integer_ | Port du point d'entrée, pour le SOCKS5 comme pour le HTTP. |
| `username` _string_ | Identifiant de connexion du proxy. |
| `password` _string_ | Mot de passe du proxy. |
| `access` _string_ | `host:port:username:password` sur une seule chaîne, prêt à donner à votre acheteur. |
| `country` _string_ | `fr`, `es` ou `eu`. |
| `expires_at` _string_ | Fin de validité (ISO 8601, UTC) : 30 jours après l'achat. |

## Idempotence

Un achat débite votre solde. Si la connexion coupe avant la réponse, vous ne pouvez pas savoir s'il a eu lieu. L'en-tête `Idempotency-Key`, obligatoire sur `POST /buy`, règle ce problème : **rejouer la même requête avec la même clé ne débite jamais deux fois**. Soit l'achat n'avait pas eu lieu et il se fait maintenant, soit il avait eu lieu et vous recevez la même commande.

| Situation | Réponse | Ce que cela signifie |
| --- | --- | --- |
| Première requête avec cette clé | 201 | Proxies achetés et débités, une seule fois. |
| Même clé, même `country` et même `qty`, commande déjà terminée | 200 et `Idempotent-Replay: true` | La commande d'origine est renvoyée. Aucun nouveau débit. |
| Même clé, première requête encore en cours de traitement | 409 `request_in_progress` | Attendez quelques secondes, puis rejouez avec la même clé. |
| Même clé avec un autre `country` ou une autre `qty` | 422 `idempotency_key_reused` | Erreur de programmation : une clé correspond à une seule commande. |
| Tentative précédente refusée sans débit (solde, stock, erreur serveur) | Nouvelle tentative normale | La clé n'est pas « consommée » : une fois le problème réglé, rejouez-la. |
| Commande débitée mais non livrée | 409 ou 500 `order_failed` | Ne rejouez pas. Contactez le support avec `order_id`. |

### Choisir la clé

Dérivez-la de l'identifiant de commande de votre propre système (`commande-4821`, `ticket-98123`...) et enregistrez-la **avant** le premier appel. Si votre programme redémarre en pleine commande, il rejouera la même clé au lieu d'en créer une nouvelle, et ne payera jamais deux fois la même commande.

Une clé fait de 8 à 128 caractères : lettres, chiffres et `. _ : -`. Les clés sont propres à votre compte. N'utilisez jamais la même clé pour deux commandes différentes, même à plusieurs semaines d'écart.

### Ce que renvoie un rejeu

La commande d'origine, reconstruite à partir de son état actuel : les accès et le solde sont ceux d'aujourd'hui. Les champs `discount` et `listed_in_resale` ne sont renvoyés qu'à la création. Seuls `country` et `qty` sont comparés : si vous rejouez avec un `price` différent, la commande d'origine est renvoyée telle quelle et son prix n'est pas modifié.

## Tarifs et remises

Le prix d'une commande dépend du pays et de la quantité (tarif dégressif). `GET /stock?qty=N` renvoie le montant exact que `POST /buy` débitera pour N proxies au moment de l'appel.

Votre remise de palier revendeur est appliquée automatiquement : vos paliers sont affichés dans l'onglet « Espace revendeur » de votre espace client. Si une remise d'avis est disponible et plus avantageuse, c'est elle qui s'applique. Les codes promo et la roue quotidienne ne s'appliquent pas aux achats par API.

La réponse de `POST /buy` indique le montant débité (`total_debited`) et la remise déjà déduite (`discount`).

## Après l'achat

### La page Revente

Les proxies achetés par API sont ajoutés automatiquement à la page Revente de votre espace revendeur, avec le prix de vente passé dans `price` (0 s'il est absent). Vous y renseignez ensuite le prix et le nom de l'acheteur quand vous le souhaitez, comme pour n'importe quel proxy.

### Utiliser un accès

Le point d'entrée (`host:port`) est le même pour tous vos proxies : c'est l'identifiant qui désigne chaque proxy. Il accepte le SOCKS5 et le HTTP (tunnel `CONNECT`, donc pour du trafic HTTPS). Reprenez toujours `host` et `port` de la réponse au lieu de les écrire en dur. Pour vérifier un accès, demandez au site quelle adresse il voit :

```bash
# SOCKS5
curl --proxy socks5h://USERNAME:PASSWORD@HOST:PORT https://airproxy.store/api/ip

# HTTP (tunnel CONNECT)
curl --proxy http://USERNAME:PASSWORD@HOST:PORT https://airproxy.store/api/ip
```

### Expiration et renouvellement

Chaque proxy expire à la date `expires_at`. Le renouvellement n'est pas disponible par l'API : faites-le depuis la page Revente de votre espace client, où les expirations proches sont signalées.

### Facture et solde

Chaque achat émet une facture, consultable dans votre espace client comme les autres. La recharge du solde se fait depuis le site, par carte ou en crypto : l'API n'a pas de route de recharge.

## Limites

| Limite | Valeur | Si elle est dépassée |
| --- | --- | --- |
| Proxies par commande | 50 | 400 `invalid_qty` |
| Proxies sur 24 heures glissantes | 200 | 429 `daily_limit`, avec `remaining_today` |
| Requêtes par minute et par clé | 30 | 429 `rate_limited` |
| Requêtes par minute et par adresse IP | 120 | 429 `rate_limited` |
| Taille du corps d'une requête | 100 Ko | 413 `payload_too_large` |

- Le plafond de 24 heures compte les proxies commandés, y compris ceux d'une commande encore en cours. Une commande refusée avant tout débit (solde, stock) ne compte pas.
- Le débit par minute compte toutes les requêtes authentifiées, y compris celles qui sont refusées.
- Chaque réponse porte les en-têtes `RateLimit-Limit`, `RateLimit-Remaining` et `RateLimit-Reset` (en secondes). Un refus `429` ajoute `Retry-After`.
- Besoin de plafonds plus élevés ? Contactez le support en précisant votre usage.

## Erreurs

Toute erreur est un objet JSON : `ok` vaut `false`, `code` est l'identifiant stable à tester, `error` un message lisible. Certains codes ajoutent des champs, signalés dans le tableau.

```json
{
  "ok": false,
  "error": "Solde insuffisant.",
  "code": "insufficient_balance",
  "balance": 1.2,
  "required": 5.4
}
```

| HTTP | Code | Signification | Que faire |
| --- | --- | --- | --- |
| 400 | `invalid_json` | Le corps de la requête n'est pas du JSON valide. | Corrigez la requête. |
| 400 | `idempotency_key_required` | En-tête `Idempotency-Key` absent ou mal formé (8 à 128 caractères : lettres, chiffres, `. _ : -`). | Ajoutez-le ou corrigez-le. |
| 400 | `invalid_country` | `country` absent, ou différent de `fr`, `es`, `eu`. | Corrigez la requête. |
| 400 | `invalid_qty` | `qty` n'est pas un entier entre 1 et 50 (ou c'est une chaîne). | Corrigez la requête. |
| 400 | `invalid_price` | `price` n'est pas un nombre entre 0 et 10 000. | Corrigez la requête. |
| 401 | `missing_key` | Aucune clé API dans la requête. | Ajoutez l'en-tête `Authorization`. |
| 401 | `invalid_key` | Clé inconnue, révoquée ou remplacée par une plus récente. | Vérifiez la clé, ou régénérez-la. |
| 402 | `insufficient_balance` | Solde insuffisant. Champs en plus : `balance`, `required`. | Rechargez, puis rejouez avec la même `Idempotency-Key`. |
| 403 | `not_reseller` | Le mode revendeur n'est pas actif sur ce compte. | Contactez le support. |
| 404 | `not_found` | Route inconnue. | Vérifiez le chemin. |
| 405 | `method_not_allowed` | Mauvaise méthode HTTP pour cette route. L'en-tête `Allow` indique la bonne. | Utilisez la méthode indiquée. |
| 409 | `out_of_stock` | Pas assez de stock pour ce pays. Champ en plus : `available`. | Réduisez `qty`, ou réessayez plus tard (la même clé reste utilisable). |
| 409 | `request_in_progress` | Une requête portant cette clé est en cours de traitement. | Attendez quelques secondes et rejouez avec la même clé. |
| 409 | `order_failed` | Cette commande n'a pas pu être menée à terme. Champ en plus : `order_id`. | Ne rejouez pas. Contactez le support avec `order_id`. |
| 413 | `payload_too_large` | Corps de requête supérieur à 100 Ko. | Réduisez le corps. |
| 415 | `unsupported_media_type` | `Content-Type` absent ou différent de `application/json`. | Envoyez `Content-Type: application/json`. |
| 422 | `idempotency_key_reused` | Cette clé a déjà servi pour un autre `country` ou une autre `qty`. | Utilisez une nouvelle clé pour cette commande. |
| 429 | `daily_limit` | Plafond de 200 proxies par 24 heures atteint. Champ en plus : `remaining_today`. | Réduisez `qty` ou attendez. |
| 429 | `rate_limited` | Trop de requêtes. | Attendez `Retry-After` secondes. |
| 500 | `order_failed` | La commande a été débitée mais n'a pas pu être livrée ou finalisée. Champ en plus : `order_id`. | Ne rejouez pas. Contactez le support avec `order_id`. |
| 500 | `server_error` | Erreur interne. | Rejouez après quelques secondes avec la même clé. |
| 502 503 504 | (corps non JSON) | Service momentanément indisponible, par exemple pendant un redémarrage. | Rejouez après quelques secondes avec la même clé. |

Les deux erreurs `order_failed` sont les seules à ne jamais rejouer : de l'argent a pu bouger sans livraison, et seul le support peut régler la commande. Pour toutes les autres erreurs réseau ou serveur, rejouer avec la même `Idempotency-Key` est sans risque.

## Exemple complet

Cette fonction achète des proxies et gère les coupures réseau, les relais indisponibles et les limites de débit en rejouant avec la même clé d'idempotence. Elle s'arrête dès qu'une erreur ne doit pas être rejouée.

**Python**

```python
import os
import time
import requests

BASE = "https://airproxy.store/api/v1/reseller"
KEY = os.environ["AIRPROXY_KEY"]
RETRY_CODES = {"request_in_progress", "server_error"}

def acheter(country, qty, idem_key, price=None, essais=5):
    """Achète des proxies. Rejouer avec la même idem_key ne débite jamais deux fois."""
    body = {"country": country, "qty": qty}
    if price is not None:
        body["price"] = price

    for essai in range(essais):
        try:
            r = requests.post(
                f"{BASE}/buy",
                json=body,
                headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": idem_key},
                timeout=30,
            )
        except requests.RequestException:
            time.sleep(2 ** essai)  # coupure réseau : on rejoue avec la MÊME clé
            continue

        try:
            data = r.json()
        except ValueError:
            data = {}  # 502, 503, 504 d'un relais : corps non JSON

        if r.status_code in (200, 201):
            return data
        if r.status_code == 429 and data.get("code") == "rate_limited":
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        if r.status_code in (502, 503, 504) or data.get("code") in RETRY_CODES:
            time.sleep(2 ** essai)
            continue
        raise RuntimeError(f"{data.get('code')}: {data.get('error')}")  # à ne pas rejouer

    raise RuntimeError(f"Abandon après {essais} essais (clé {idem_key})")

# Servir un acheteur : la clé est dérivée de VOTRE identifiant de commande
commande = acheter("fr", 3, idem_key="commande-4821")
for p in commande["proxies"]:
    print(p["access"])
```

**Node.js**

```js
// Node.js 18 ou plus, module ES (fichier .mjs)
const BASE = "https://airproxy.store/api/v1/reseller";
const KEY = process.env.AIRPROXY_KEY;
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Rejouer avec la même idemKey ne débite jamais deux fois.
async function acheter({ country, qty, price }, idemKey, essais = 5) {
  const body = { country, qty, ...(price != null && { price }) };

  for (let essai = 0; essai < essais; essai++) {
    let res;
    try {
      res = await fetch(`${BASE}/buy`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${KEY}`,
          "Idempotency-Key": idemKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(30000),
      });
    } catch {
      await pause(2 ** essai * 1000); // coupure réseau : on rejoue avec la MÊME clé
      continue;
    }

    const data = await res.json().catch(() => ({})); // 502, 503, 504 d'un relais : corps non JSON
    if (res.ok) return data;

    if (res.status === 429 && data.code === "rate_limited") {
      await pause(Number(res.headers.get("retry-after") ?? 5) * 1000);
      continue;
    }
    if ([502, 503, 504].includes(res.status) || ["request_in_progress", "server_error"].includes(data.code)) {
      await pause(2 ** essai * 1000);
      continue;
    }
    throw new Error(`${data.code}: ${data.error}`); // à ne pas rejouer
  }
  throw new Error(`Abandon après ${essais} essais (clé ${idemKey})`);
}

// Servir un acheteur : la clé est dérivée de VOTRE identifiant de commande
const commande = await acheter({ country: "fr", qty: 3 }, "commande-4821");
commande.proxies.forEach((p) => console.log(p.access));
```

## Bonnes pratiques

- **Une clé d'idempotence par commande, enregistrée avant l'appel.** Dérivez-la de l'identifiant de commande de votre système.
- **Ne rejouez jamais un `order_failed`.** Contactez le support avec le `order_id`.
- **Traitez `out_of_stock` comme une réponse normale.** Prévenez votre acheteur ou réessayez plus tard, sans boucle serrée.
- **Gardez un solde suffisant pour la nuit.** Un `insufficient_balance` ne débite rien : après la recharge, rejouez la même clé.
- **Conservez les accès livrés** avec la référence de commande, dans votre base. Un rejeu renvoie l'état actuel de la commande, pas son état d'origine.
- **N'interrogez pas le stock en boucle.** Un appel toutes les quelques secondes suffit largement, et vous disposez de 30 requêtes par minute.
- **Surveillez `expires_at`** et renouvelez avant l'échéance depuis votre espace client.
- **Gardez la clé secrète** et régénérez-la au moindre doute.

## Support

Une question, ou un comportement inattendu ? Ouvrez un ticket depuis votre espace client, ou écrivez-nous sur Discord. Indiquez la référence `order_id` de la commande concernée, l'heure de l'appel et le code d'erreur reçu : cela nous permet de retrouver la requête tout de suite.

### Historique

3 octobre 2026 : première version, avec les routes `balance`, `stock` et `buy`.
