# ARC Proxies API — guide for coding agents

You are integrating the ARC Proxies HTTP API. This file is the complete
reference; you do not need any other source. If something here contradicts a
memory or an assumption, this file wins.

- Base URL: `https://api.arcproxy.cloud/api/v1`
- Spec: `https://api.arcproxy.cloud/api/v1/openapi.json`
- Auth: an API key from the ARC dashboard (Sidebar → API → Create API key)
- Everything is JSON. Every path also answers with a trailing slash.

## Authentication

Send the key on every request, either way:

```
Authorization: Bearer arc_live_xxxxxxxx
X-API-Key: arc_live_xxxxxxxx
```

Keys start with `arc_live_`, are shown once at creation, and carry scopes:

- `read` — `/account`, `/plans`, `/balance`, `/usage`, `/orders`
- `proxies` — `POST /proxies`

A key missing the scope gets `403`, not `401`. Keys cannot create or revoke
other keys; that is dashboard-only.

## Read this before you write code

1. **Generate proxy endpoints once and reuse them.** They do not expire. Do
   NOT call `POST /proxies` per request, per thread, or in a retry loop — that
   is the single most common way to hit the rate limit.
2. **Bandwidth is what costs money, not API calls.** Billing is per GB of
   traffic through the proxy. Generating 10 000 endpoints costs nothing;
   sending 10 000 requests through them costs bandwidth.
3. **`session_ttl` is in SECONDS** for every plan, even though the underlying
   gateway thinks in minutes. Do not pass minutes.
4. **`country` is a lowercase ISO 3166-1 alpha-2 code** (`de`, `us`, `gb`).
   Not a country name, not `DEU`.
5. **Keep the key out of source control and out of logs.** Read it from an
   environment variable. A leaked key can spend the account's bandwidth.
6. **Errors are always the same shape.** Branch on `error.type`, never on the
   text of `error.message`.

## Plans

| plan_id | Name | Pool | €/GB (net) | Countries | Cities | fast_mode |
|---|---|---|---|---|---|---|
| `arc-basic` | ARC Basic | isp | €0.75 | any ISO-2 | yes | yes |
| `arc-fast` | ARC Fast | residential | €2.00 | any ISO-2 | yes | yes |
| `arc-ultra` | ARC Ultra | residential | €3.50 | any ISO-2 | yes | yes |
| `arc-ultimate` | ARC Ultimate | residential | €4.50 | any ISO-2 | yes | yes |
| `arc-mobile` | ARC Mobile | mobile | €3.50 | DE/GB/US/JP | no | no |

Volume discounts apply per plan and are returned by `GET /plans` as
`volume_tiers`. Prices exclude VAT.


`arc-mobile` is special: its endpoints are minted by the carrier upstream, so
it needs `country`, ignores `city` and `fast_mode`, and its `session_ttl` is
capped at 300–3600 seconds.

## Endpoints

### `GET /account`
Who the key belongs to.

```json
{"id": 12, "email": "you@example.com", "name": "You",
 "created_at": "2026-01-04T09:12:00Z",
 "api_key": {"name": "scraper", "prefix": "arc_live_a6N6yE",
             "scopes": ["read", "proxies"], "expires_at": null}}
```

### `GET /plans`
The catalog. Use it to discover plan ids and per-plan capabilities instead of
hardcoding them.

```json
{"plans": [{"id": "arc-fast", "name": "ARC Fast", "price_per_gb": 2.0,
            "currency": "eur", "pool_type": "residential",
            "min_gb": 1, "max_gb": 1000,
            "countries": [], "supports_cities": true,
            "supports_fast_mode": true, "provider_generated": false,
            "volume_tiers": [{"min_gb": 25, "off": 0.05}]}]}
```

`countries: []` means any ISO-2 code is accepted.

### `GET /balance`
What is left to spend. These are the upstream's own numbers, refreshed on the
way in — poll at most once a minute, not per request.

```json
{"plans": [{"plan_id": "arc-fast", "plan_name": "ARC Fast",
            "pool_type": "residential", "purchased_gb": 50.0,
            "used_gb": 12.4, "remaining_gb": 37.6, "pending_gb": 0.0}],
 "totals": {"purchased_gb": 50.0, "used_gb": 12.4,
            "remaining_gb": 37.6, "pending_gb": 0.0}}
```

`pending_gb` is bandwidth that is paid for but not yet allocated upstream. It
is NOT spendable yet; do not add it to `remaining_gb`.

### `GET /usage`
Consumption over time, one series per plan.

Query: `range=24h|30d|12m` (default `30d`), optional
`pool_type=residential|isp|mobile`.

```json
{"range": "30d", "granularity": "day", "unit": "gb",
 "series": [{"key": "arc-fast", "label": "ARC Fast"}],
 "points": [{"t": "2026-08-29", "arc-fast": 1.42}],
 "total_gb": 12.4}
```

Each point is the usage IN that bucket, not a running total.

### `GET /orders`
Paid top-ups, newest first. Query: `limit` (default 50, max 200), `offset`.

```json
{"data": [{"id": 88, "plan_id": "arc-fast", "plan_name": "ARC Fast",
           "gb": 50, "amount_cents": 9200, "currency": "eur",
           "status": "paid", "review_state": "not_required",
           "created_at": "2026-08-01T10:00:00Z",
           "items": [{"plan_id": "arc-fast", "gb": 50}]}],
 "total": 1, "limit": 50, "offset": 0}
```

`status` is about the money; `review_state` is about delivery. A large order is
paid immediately but `held` until a human releases it, so its GB do not appear
in `/balance` yet — they are reported as `pending_gb`. The values are
`not_required` (the usual case), `held`, `approved` and `declined`. If you are
waiting on bandwidth to appear, this field is what tells you whether to keep
waiting or to get in touch.

There is no endpoint that buys bandwidth. Purchases happen in the dashboard;
the API is read-only about money.

## `POST /proxies` — the one that matters

Requires the `proxies` scope. Returns endpoints you connect through.

### Request

| Field | Type | Default | Notes |
|---|---|---|---|
| `plan_id` | string | — | **Required.** See the plan table. |
| `count` | integer | `1` | 1–10000. |
| `country` | string or array | none | Lowercase ISO-2. An array spreads the batch round-robin. Required for `arc-mobile`. |
| `city` | string or array | none | Rotating plans only, e.g. `"Berlin"`. Ignored without a country. |
| `session` | `rotating` \| `sticky` | `rotating` | `sticky` holds one exit IP. |
| `session_ttl` | integer (**seconds**) | `600` | How long a sticky IP is held. Rotating plans 60–86400; `arc-mobile` 300–3600. |
| `session_ids` | array of strings | none | Reuse handles from an earlier response to get the SAME exit IPs back. Cycled if shorter than `count`. |
| `fast_mode` | boolean | `false` | Rotating plans only. Prefers the fastest nodes. Free. |
| `protocol` | `http` \| `socks5` | `http` | |
| `format` | `url` \| `hostport` \| `userpass` | `url` | `url` → `http://user:pass@host:port`, `hostport` → `host:port:user:pass`, `userpass` → `user:pass:host:port`. |

### Response

```json
{"plan": {"id": "arc-fast", "name": "ARC Fast", "pool_type": "residential"},
 "count": 1, "format": "url", "protocol": "http", "session": "sticky",
 "balance": {"purchased_gb": 50.0, "used_gb": 12.4,
             "remaining_gb": 37.6, "pending_gb": 0.0},
 "proxies": ["http://arc-42_pool-arcfast_country-de_session-k3f9a1_sesstime-10:pw@proxy.arcproxies.io:8000"],
 "endpoints": [{"host": "proxy.arcproxies.io", "port": 8000,
                "username": "arc-42_pool-arcfast_country-de_session-k3f9a1_sesstime-10",
                "password": "pw", "country": "de", "city": null,
                "session_id": "k3f9a1", "proxy": "http://…"}]}
```

- `proxies` — ready-to-paste strings in the requested `format`. Use these.
- `endpoints` — the same endpoints split into fields, for clients that want
  host/port/user/pass separately.
- `balance` — always present, zeros if the plan has no allocation yet.
- `session_id` — only for `session: "sticky"`, and `null` for `arc-mobile`
  (the carrier embeds the session in the username it returns).

### Sticky sessions

To come back to the same exit IPs later, store `endpoints[].session_id` and
pass them as `session_ids` on the next call with the same `plan_id` and
`country`. The IP is held for `session_ttl` seconds of inactivity; a handle
reused after that gets a fresh IP under the same name.

## Errors

Every failure, at every status code, has this body:

```json
{"error": {"type": "invalid_request_error",
           "message": "`count` must be between 1 and 10000.",
           "param": "count",
           "docs_url": "https://dashboard.arcproxies.io/api"}}
```

| Status | `error.type` | What to do |
|---|---|---|
| 400 | `invalid_request_error` | Fix the field named in `param`. Do not retry unchanged. |
| 401 | `authentication_error` | Key missing, wrong, revoked or expired. Do not retry. |
| 403 | `permission_error` | The key lacks the scope. Issue a new key. Do not retry. |
| 409 | `conflict_error` | No bandwidth for that plan on this account yet. Buy some in the dashboard. |
| 429 | `rate_limit_error` | Sleep for `Retry-After` seconds, then retry once. |
| 502 | `upstream_error` | Transient supplier problem. Retry with backoff, max 3 times. |

## Rate limits

Counted per API key.

- 120/min on the read endpoints
- 30/min on `POST /proxies`

Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. A 429
carries `Retry-After` in seconds. Honour it — retrying immediately just burns
the next window too.

## Working examples

### Python

```python
import os
import requests

API = "https://api.arcproxy.cloud/api/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['ARC_API_KEY']}"


def check_balance(plan_id: str) -> float:
    data = SESSION.get(f"{API}/balance", timeout=30).json()
    row = next((p for p in data["plans"] if p["plan_id"] == plan_id), None)
    return row["remaining_gb"] if row else 0.0


def get_proxies(plan_id: str, count: int, country: str) -> list[str]:
    res = SESSION.post(
        f"{API}/proxies",
        json={
            "plan_id": plan_id,
            "count": count,
            "country": country,
            "session": "sticky",
            "session_ttl": 600,   # SECONDS
        },
        timeout=60,
    )
    if res.status_code != 200:
        raise RuntimeError(res.json()["error"]["message"])
    return res.json()["proxies"]


# Generate ONCE, then reuse for the whole run.
pool = get_proxies("arc-fast", 25, "de")
for i, url in enumerate(pool):
    r = requests.get("https://example.com",
                     proxies={"http": url, "https": url}, timeout=30)
    print(i, r.status_code)
```

### Node

```js
const API = "https://api.arcproxy.cloud/api/v1"
const headers = {
  Authorization: `Bearer ${process.env.ARC_API_KEY}`,
  "Content-Type": "application/json",
}

async function getProxies(planId, count, country) {
  const res = await fetch(`${API}/proxies`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      plan_id: planId,
      count,
      country,
      session: "rotating",
      format: "url",
    }),
  })
  const body = await res.json()
  if (!res.ok) throw new Error(body.error.message)
  return body.proxies
}

const pool = await getProxies("arc-fast", 25, "de")
```

### curl

```bash
curl -s https://api.arcproxy.cloud/api/v1/balance -H "Authorization: Bearer $ARC_API_KEY"

curl -s https://api.arcproxy.cloud/api/v1/proxies \
  -H "Authorization: Bearer $ARC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan_id":"arc-fast","count":25,"country":"de"}'
```

## Checklist before you call this done

- [ ] The key is read from an environment variable, never a literal.
- [ ] `POST /proxies` is called once per run, not per request.
- [ ] `session_ttl` is expressed in seconds.
- [ ] `country` is a lowercase two-letter code.
- [ ] 429 is handled by sleeping for `Retry-After`.
- [ ] 400/401/403 are treated as permanent — no retry loop.
- [ ] Balance is checked before a long run, not on every request.
