Contents Retour à l'espace client

Reseller API

Buy proxies with your balance from your own software, at any time, without going through the site. The credentials come back in the response, and the proxies appear in your reseller area right away.

Télécharger la documentation (.md)
Base URL
https://airproxy.store/api/v1/reseller
Authentication
Authorization: Bearer VOTRE_CLE_API
Format
JSON in UTF-8, amounts in euros, dates in ISO 8601 (UTC)
Version
v1 (expect fields to be added to responses)

Overview

The API automates what you do by hand from the buy page: read your balance, check stock and the price of an order, then buy ISP proxies for France, Spain or Europe. Each proxy is valid for 30 days.

An API purchase behaves exactly like a balance purchase on the site:

  • the amount is debited from your balance, with your tier discount;
  • an invoice is issued, as for any purchase;
  • the proxies are created on your account and their credentials are returned in the response;
  • they are added to the Resale page of your reseller area, with a sale price of 0 that you then set by hand (or pass in the request).
MethodRouteRole
GET/balanceYour current balance.
GET/stockAvailability and price of an order of N proxies, country by country.
POST/buyBuying proxies: debit the balance and deliver the credentials immediately.

What the API does not do. Renewing and resetting a proxy do not go through the API: do them from your client area.

Quick start

  1. Generate your key. In your client area, open the "Reseller area" tab, expand the "Automatic purchase API" block and click "Generate a key". Copy it right away: it is shown only once.
  2. Check that it works by reading your balance.
  3. Place a first order.

Reading the balance:

export AIRPROXY_KEY="apr_votre_cle_ici"

curl https://airproxy.store/api/v1/reseller/balance \
  -H "Authorization: Bearer $AIRPROXY_KEY"
{ "ok": true, "balance": 94.3, "currency": "EUR" }

Buying 2 French proxies:

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

There is no test environment. Every purchase is real and debited from your balance. To check your integration, order a single proxy.

On Windows, replace $(uuidgen) with any unique string of 8 characters or more. The addresses in the examples (203.0.113.10) are fictitious: always use the host and port from the response.

Authentication

Every request carries your API key in the Authorization header. The X-API-Key header is accepted instead.

Authorization: Bearer apr_votre_cle_ici
  • There is a single key per account. Generating a new one invalidates the old one immediately.
  • We do not store the key in clear text: if you lose it, regenerate one.
  • It can buy with your balance and nothing else. It gives access neither to your password, nor to your invoices, nor to your payment methods.
  • You can revoke it at any time from the same block of your reseller area.
  • It stops working (403 not_reseller) if reseller mode is removed from your account.

Keep the key on the server side: an environment variable or a secrets manager, never in a web page, a mobile app or a public repository. The API actually refuses calls from a browser.

A stolen key can buy with your balance, up to the caps described below. If it has leaked, regenerate it at once, and keep only the balance you need on the account.

Conventions

  • All routes are over HTTPS. Requests with a body send Content-Type: application/json.
  • A successful response contains "ok": true. An error returns {"ok": false, "error": "...", "code": "..."} with the matching HTTP status and, depending on the case, extra fields (balance, available...). Base your logic on code, which is stable, never on error, which is text for humans.
  • Amounts are numbers in euros, with at most two decimals. Dates are in ISO 8601 format, in UTC.
  • Types are strict: a number is passed as a JSON number ("qty": 2, not "qty": "2"). An ambiguous value is refused rather than interpreted, because this API debits money.
  • Unknown fields in a request are ignored. Do the same with responses: fields may be added to them.
  • Responses are never cached (Cache-Control: no-store): they contain credentials.
  • The API is meant to be called from your server: calls from a browser are blocked.

Routes

The paths below are relative to the base URL https://airproxy.store/api/v1/reseller. The examples read your key from the AIRPROXY_KEY variable.

GET/balance

Returns your current balance.

curl https://airproxy.store/api/v1/reseller/balance \
  -H "Authorization: Bearer $AIRPROXY_KEY"
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())
const res = await fetch("https://airproxy.store/api/v1/reseller/balance", {
  headers: { Authorization: `Bearer ${process.env.AIRPROXY_KEY}` },
});
console.log(await res.json());

200 response

FieldDescription
ok booleantrue.
balance numberYour balance, in euros.
currency stringAlways EUR.
{ "ok": true, "balance": 94.3, "currency": "EUR" }

GET/stock

Returns, for each country, the number of available proxies and the price of an order of qty proxies. Worth checking before a large purchase: pricing is tiered, so the unit price depends on the quantity.

Query parameter

NameDescription
qty integer optionalNumber of proxies to compute the price for, from 1 to 50. Absent: 1. A value outside this range is clamped to the nearest limit.
curl "https://airproxy.store/api/v1/reseller/stock?qty=5" \
  -H "Authorization: Bearer $AIRPROXY_KEY"
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())
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());

200 response

FieldDescription
ok booleantrue.
currency stringAlways EUR.
stock arrayOne entry per offered country: fr, es, eu.
stock[].country stringCountry code, to be used as-is in POST /buy.
stock[].available integerNumber of proxies available for this country. If this number is lower than qty, the purchase fails with out_of_stock.
stock[].qty integerThe quantity used for the computation (the one in the request, clamped between 1 and 50).
stock[].total numberAmount that POST /buy would debit at this moment for qty proxies of this country, your discount included.
stock[].unit_price numberPrice per proxy for this quantity (total divided by qty).
{
  "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 }
  ]
}

The values in this example are fictitious. Stock is shared across all clients: available may change from one call to the next.

POST/buy

Buys qty proxies of a country, for 30 days, debiting your balance. The order is all-or-nothing: if the balance or stock is not enough, nothing is debited.

Headers

HeaderDescription
Authorization requiredBearer VOTRE_CLE_API
Idempotency-Key requiredUnique identifier of the order, from 8 to 128 characters (letters, digits and . _ : -). See Idempotency.
Content-Type requiredapplication/json

Request body

FieldDescription
country string requiredfr, es or eu (case is ignored).
qty integer requiredNumber of proxies, from 1 to 50. A JSON number, not a string.
price number optionalSale price to record on the Resale page, in euros (from 0 to 10,000). Absent: 0, and you set it later from your area.
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}'
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"])
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);

In these examples, the idempotency key is derived from the order identifier of your own system, which is the best practice (see Idempotency).

201 response

The order is created. Replaying the same request returns status 200 with the header Idempotent-Replay: true.

FieldDescription
ok booleantrue.
order_id integerOrder reference. Give it to support if there is a problem.
country stringCountry ordered.
qty integerNumber of proxies ordered.
total_debited numberAmount debited from your balance, in euros, discount deducted.
proxies arrayThe delivered proxies, described below.
discount numberDiscount applied, in euros, already deducted from total_debited. Returned on creation only.
balance numberYour balance after the purchase. On a replay: your current balance.
listed_in_resale booleanfalse if the automatic addition to the Resale page failed. The proxies are delivered in any case. Returned on creation only.

proxies[] object

FieldDescription
proxy_id stringProxy identifier in your area.
host stringEntry point address.
port integerEntry point port, for SOCKS5 as well as HTTP.
username stringProxy login username.
password stringProxy password.
access stringhost:port:username:password on a single string, ready to give to your buyer.
country stringfr, es or eu.
expires_at stringEnd of validity (ISO 8601, UTC): 30 days after the purchase.

Idempotency

A purchase debits your balance. If the connection drops before the response, you cannot know whether it happened. The Idempotency-Key header, required on POST /buy, solves this: replaying the same request with the same key never debits twice. Either the purchase had not happened and it happens now, or it had happened and you receive the same order.

SituationResponseWhat it means
First request with this key201Proxies bought and debited, once only.
Same key, same country and same qty, order already completed200 and Idempotent-Replay: trueThe original order is returned. No new debit.
Same key, first request still being processed409 request_in_progressWait a few seconds, then replay with the same key.
Same key with a different country or a different qty422 idempotency_key_reusedProgramming error: a key maps to a single order.
Previous attempt refused without a debit (balance, stock, server error)A normal new attemptThe key is not "consumed": once the problem is fixed, replay it.
Order debited but not delivered409 or 500 order_failedDo not replay. Contact support with order_id.

Choosing the key

Derive it from the order identifier of your own system (order-4821, ticket-98123...) and save it before the first call. If your program restarts mid-order, it will replay the same key instead of creating a new one, and will never pay twice for the same order.

A key is 8 to 128 characters: letters, digits and . _ : -. Keys are specific to your account. Never use the same key for two different orders, even weeks apart.

What a replay returns

The original order, rebuilt from its current state: the credentials and balance are today's. The discount and listed_in_resale fields are returned on creation only. Only country and qty are compared: if you replay with a different price, the original order is returned as-is and its price is not changed.

Pricing and discounts

The price of an order depends on the country and the quantity (tiered pricing). GET /stock?qty=N returns the exact amount POST /buy will debit for N proxies at the time of the call.

Your reseller tier discount is applied automatically: your tiers are shown in the "Reseller area" tab of your client area. If a review discount is available and more advantageous, that one applies. Promo codes and the daily wheel do not apply to API purchases.

The POST /buy response indicates the amount debited (total_debited) and the discount already deducted (discount).

After the purchase

The Resale page

Proxies bought via the API are added automatically to the Resale page of your reseller area, with the sale price passed in price (0 if absent). You then set the price and the buyer's name there whenever you want, as for any proxy.

Using a credential

The entry point (host:port) is the same for all your proxies: it is the username that designates each proxy. It accepts SOCKS5 and HTTP (CONNECT tunnel, so for HTTPS traffic). Always reuse host and port from the response instead of hard-coding them. To check a credential, ask the site which address it sees:

# 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

Expiry and renewal

Each proxy expires on the expires_at date. Renewal is not available through the API: do it from the Resale page of your client area, where upcoming expiries are flagged.

Invoice and balance

Each purchase issues an invoice, viewable in your client area like the others. Topping up the balance is done from the site, by card or crypto: the API has no top-up route.

Limits

LimitValueIf exceeded
Proxies per order50400 invalid_qty
Proxies over a rolling 24 hours200429 daily_limit, with remaining_today
Requests per minute per key30429 rate_limited
Requests per minute per IP address120429 rate_limited
Request body size100 KB413 payload_too_large
  • The 24-hour cap counts the proxies ordered, including those of an order still in progress. An order refused before any debit (balance, stock) does not count.
  • The per-minute rate counts all authenticated requests, including those that are refused.
  • Each response carries the headers RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (in seconds). A 429 refusal adds Retry-After.
  • Need higher caps? Contact support, stating your use case.

Errors

Every error is a JSON object: ok is false, code is the stable identifier to test, error a readable message. Some codes add fields, noted in the table.

{
  "ok": false,
  "error": "Solde insuffisant.",
  "code": "insufficient_balance",
  "balance": 1.2,
  "required": 5.4
}
HTTPCodeMeaningWhat to do
400invalid_jsonThe request body is not valid JSON.Fix the request.
400idempotency_key_requiredIdempotency-Key header missing or malformed (8 to 128 characters: letters, digits, . _ : -).Add it or fix it.
400invalid_countrycountry missing, or other than fr, es, eu.Fix the request.
400invalid_qtyqty is not an integer between 1 and 50 (or it is a string).Fix the request.
400invalid_priceprice is not a number between 0 and 10,000.Fix the request.
401missing_keyNo API key in the request.Add the Authorization header.
401invalid_keyUnknown key, revoked, or replaced by a more recent one.Check the key, or regenerate it.
402insufficient_balanceInsufficient balance. Extra fields: balance, required.Top up, then replay with the same Idempotency-Key.
403not_resellerReseller mode is not active on this account.Contact support.
404not_foundUnknown route.Check the path.
405method_not_allowedWrong HTTP method for this route. The Allow header indicates the right one.Use the indicated method.
409out_of_stockNot enough stock for this country. Extra field: available.Reduce qty, or try again later (the same key stays usable).
409request_in_progressA request carrying this key is being processed.Wait a few seconds and replay with the same key.
409order_failedThis order could not be completed. Extra field: order_id.Do not replay. Contact support with order_id.
413payload_too_largeRequest body larger than 100 KB.Reduce the body.
415unsupported_media_typeContent-Type missing or other than application/json.Send Content-Type: application/json.
422idempotency_key_reusedThis key was already used for a different country or qty.Use a new key for this order.
429daily_limitCap of 200 proxies per 24 hours reached. Extra field: remaining_today.Reduce qty or wait.
429rate_limitedToo many requests.Wait Retry-After seconds.
500order_failedThe order was debited but could not be delivered or finalized. Extra field: order_id.Do not replay. Contact support with order_id.
500server_errorInternal error.Replay after a few seconds with the same key.
502 503 504(non-JSON body)Service momentarily unavailable, for example during a restart.Replay after a few seconds with the same key.

The two order_failed errors are the only ones never to replay: money may have moved without delivery, and only support can settle the order. For all other network or server errors, replaying with the same Idempotency-Key is safe.

Full example

This function buys proxies and handles network drops, unavailable upstreams and rate limits by replaying with the same idempotency key. It stops as soon as an error must not be replayed.

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

Best practices

  • One idempotency key per order, saved before the call. Derive it from your system's order identifier.
  • Never replay an order_failed. Contact support with the order_id.
  • Treat out_of_stock as a normal response. Warn your buyer or try again later, without a tight loop.
  • Keep enough balance for overnight. An insufficient_balance debits nothing: after topping up, replay the same key.
  • Store the delivered credentials with the order reference, in your database. A replay returns the current state of the order, not its original state.
  • Do not poll the stock in a loop. One call every few seconds is more than enough, and you have 30 requests per minute.
  • Watch expires_at and renew before the deadline from your client area.
  • Keep the key secret and regenerate it at the slightest doubt.

Support

A question, or unexpected behavior? Open a ticket from your client area, or write to us on Discord. State the order_id reference of the order concerned, the time of the call and the error code received: this lets us find the request right away.

History

: first version, with the balance, stock and buy routes.