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).
| Method | Route | Role |
|---|---|---|
| GET | /balance | Your current balance. |
| GET | /stock | Availability and price of an order of N proxies, country by country. |
| POST | /buy | Buying 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
- 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.
- Check that it works by reading your balance.
- 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 oncode, which is stable, never onerror, 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
| Field | Description |
|---|---|
ok boolean | true. |
balance number | Your balance, in euros. |
currency string | Always 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
| Name | Description |
|---|---|
qty integer optional | Number 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
| Field | Description |
|---|---|
ok boolean | true. |
currency string | Always EUR. |
stock array | One entry per offered country: fr, es, eu. |
stock[].country string | Country code, to be used as-is in POST /buy. |
stock[].available integer | Number of proxies available for this country. If this number is lower than qty, the purchase fails with out_of_stock. |
stock[].qty integer | The quantity used for the computation (the one in the request, clamped between 1 and 50). |
stock[].total number | Amount that POST /buy would debit at this moment for qty proxies of this country, your discount included. |
stock[].unit_price number | Price 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
| Header | Description |
|---|---|
Authorization required | Bearer VOTRE_CLE_API |
Idempotency-Key required | Unique identifier of the order, from 8 to 128 characters (letters, digits and . _ : -). See Idempotency. |
Content-Type required | application/json |
Request body
| Field | Description |
|---|---|
country string required | fr, es or eu (case is ignored). |
qty integer required | Number of proxies, from 1 to 50. A JSON number, not a string. |
price number optional | Sale 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.
| Field | Description |
|---|---|
ok boolean | true. |
order_id integer | Order reference. Give it to support if there is a problem. |
country string | Country ordered. |
qty integer | Number of proxies ordered. |
total_debited number | Amount debited from your balance, in euros, discount deducted. |
proxies array | The delivered proxies, described below. |
discount number | Discount applied, in euros, already deducted from total_debited. Returned on creation only. |
balance number | Your balance after the purchase. On a replay: your current balance. |
listed_in_resale boolean | false if the automatic addition to the Resale page failed. The proxies are delivered in any case. Returned on creation only. |
proxies[] object
| Field | Description |
|---|---|
proxy_id string | Proxy identifier in your area. |
host string | Entry point address. |
port integer | Entry point port, for SOCKS5 as well as HTTP. |
username string | Proxy login username. |
password string | Proxy password. |
access string | host:port:username:password on a single string, ready to give to your buyer. |
country string | fr, es or eu. |
expires_at string | End 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.
| Situation | Response | What it means |
|---|---|---|
| First request with this key | 201 | Proxies bought and debited, once only. |
Same key, same country and same qty, order already completed | 200 and Idempotent-Replay: true | The original order is returned. No new debit. |
| Same key, first request still being processed | 409 request_in_progress | Wait a few seconds, then replay with the same key. |
Same key with a different country or a different qty | 422 idempotency_key_reused | Programming error: a key maps to a single order. |
| Previous attempt refused without a debit (balance, stock, server error) | A normal new attempt | The key is not "consumed": once the problem is fixed, replay it. |
| Order debited but not delivered | 409 or 500 order_failed | Do 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
| Limit | Value | If exceeded |
|---|---|---|
| Proxies per order | 50 | 400 invalid_qty |
| Proxies over a rolling 24 hours | 200 | 429 daily_limit, with remaining_today |
| Requests per minute per key | 30 | 429 rate_limited |
| Requests per minute per IP address | 120 | 429 rate_limited |
| Request body size | 100 KB | 413 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-RemainingandRateLimit-Reset(in seconds). A429refusal addsRetry-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
}
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_json | The request body is not valid JSON. | Fix the request. |
| 400 | idempotency_key_required | Idempotency-Key header missing or malformed (8 to 128 characters: letters, digits, . _ : -). | Add it or fix it. |
| 400 | invalid_country | country missing, or other than fr, es, eu. | Fix the request. |
| 400 | invalid_qty | qty is not an integer between 1 and 50 (or it is a string). | Fix the request. |
| 400 | invalid_price | price is not a number between 0 and 10,000. | Fix the request. |
| 401 | missing_key | No API key in the request. | Add the Authorization header. |
| 401 | invalid_key | Unknown key, revoked, or replaced by a more recent one. | Check the key, or regenerate it. |
| 402 | insufficient_balance | Insufficient balance. Extra fields: balance, required. | Top up, then replay with the same Idempotency-Key. |
| 403 | not_reseller | Reseller mode is not active on this account. | Contact support. |
| 404 | not_found | Unknown route. | Check the path. |
| 405 | method_not_allowed | Wrong HTTP method for this route. The Allow header indicates the right one. | Use the indicated method. |
| 409 | out_of_stock | Not enough stock for this country. Extra field: available. | Reduce qty, or try again later (the same key stays usable). |
| 409 | request_in_progress | A request carrying this key is being processed. | Wait a few seconds and replay with the same key. |
| 409 | order_failed | This order could not be completed. Extra field: order_id. | Do not replay. Contact support with order_id. |
| 413 | payload_too_large | Request body larger than 100 KB. | Reduce the body. |
| 415 | unsupported_media_type | Content-Type missing or other than application/json. | Send Content-Type: application/json. |
| 422 | idempotency_key_reused | This key was already used for a different country or qty. | Use a new key for this order. |
| 429 | daily_limit | Cap of 200 proxies per 24 hours reached. Extra field: remaining_today. | Reduce qty or wait. |
| 429 | rate_limited | Too many requests. | Wait Retry-After seconds. |
| 500 | order_failed | The order was debited but could not be delivered or finalized. Extra field: order_id. | Do not replay. Contact support with order_id. |
| 500 | server_error | Internal 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 theorder_id. - Treat
out_of_stockas a normal response. Warn your buyer or try again later, without a tight loop. - Keep enough balance for overnight. An
insufficient_balancedebits 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_atand 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.
