# Basil Proxies — Partner API v1

<!-- Doc version v1.2 (2026-08). This file has TWO copies that must stay
     byte-identical: backend `docs/RESELLER_API.md` and the portal's
     `public/docs/RESELLER_API.md`. Edit one, copy to the other.

     Audience markers (2026-08-24): the portal's docs page renders
     `pool1:start/end` blocks only for partners with classic-residential
     history, and `res2only:start/end` blocks only for everyone else. Keep
     each marker on its own line with blank lines around the block, and never
     place one inside a table or list. -->

Wholesale residential-proxy management for Basil partners: create end-customer
proxy accounts, allocate and reclaim bandwidth from your pool, read usage, and
rotate credentials. Your portal and this API are the same thing — anything the
portal shows, you can automate.

<!-- pool1:start -->

**Two products** are available, each with its own pool:

| `product` | name | entry points | targeting rides |
|---|---|---|---|
| `residential2` | Residential 2.0 — the main pool | rotating + sticky HTTP | the username |
| `residential` (default) | Residential | HTTP + SOCKS5 | the password |

Every money, credential and usage call takes an optional **`product`** — in
the JSON body on POSTs, as `?product=` on GETs. Omitting it means
`residential`, so code written against v1.0/v1.1 keeps working unchanged.

<!-- pool1:end -->

<!-- res2only:start -->

Your product is **Residential 2.0** (`product: "residential2"`): rotating +
sticky HTTP entry points, targeting on the username.

Every money, credential and usage call takes a **`product`** — in the JSON
body on POSTs, as `?product=` on GETs. **Always send `residential2`
explicitly, on every call** — the parameter is what routes the operation to
your pool and your customers' accounts.

<!-- res2only:end -->


```
Base URL:  https://api.basilproxies.com/api/reseller/v1
Auth:      Authorization: Bearer bp_rk_live_...
Content:   application/json (UTF-8)
```

All bandwidth numbers are **MB, and 1 GB = 1000 MB**. Requests accept
`amountMb` **or** `amountGb` — send one, not both.

---

## 1. Authentication

Every request needs an API key in the `Authorization` header:

```bash
curl -s https://api.basilproxies.com/api/reseller/v1/me \
  -H "Authorization: Bearer bp_rk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

* Keys are issued by Basil (and later self-service in the portal). The full
  secret is shown **once** at creation — we store only a hash.
* You can hold **up to 2 active keys** — create the second, migrate, revoke
  the first for zero-downtime rotation.
* A suspended partner account answers `403 RESELLER_SUSPENDED` on every
  endpoint until reactivated.

## 2. Rate limits

* Your account has a per-minute request ceiling (default **120/min**, shown in
  `GET /me`, raisable on request — sneaker-drop bursts are a known workload).
* Exceeding it returns `429 RATE_LIMITED` with a `Retry-After` header
  (seconds). Back off and retry — nothing is lost.
* `?fresh=true` live-balance reads have a separate, much smaller budget
  (default **6/min**) because they hit the upstream network directly. The
  default cached read (~45 s freshness) is the right choice for polling.

## 3. Idempotency (recommended on every mutation)

Send an `Idempotency-Key` header (any unique string ≤ 200 chars, e.g. a UUID
or your order id) on POST/DELETE requests. If the same key is sent again:

* same request → you get the **stored original response** (marked with an
  `Idempotency-Replayed: true` header), the operation does **not** run twice;
* different body or endpoint → `409 IDEMPOTENCY_CONFLICT`;
* original still executing → `409 REQUEST_IN_FLIGHT` (retry shortly).

Definitive outcomes — including 4xx failures like `INSUFFICIENT_POOL` — are
stored and replayed as-is. A retried failure stays failed on that key; after
fixing the cause, retry with a **new** key.

Failure semantics on the same key:

* `503 SYSTEM_BUSY` / `503 RESELLER_NOT_CONFIGURED` — nothing ran; the key is
  released and **may be retried as-is**.
* Any other unexpected `5xx` — the request may have partially executed, so
  the key stays claimed: replays answer `409 REQUEST_IN_FLIGHT`. Verify the
  resource state with a GET, then continue with a **new** key. (Committed
  money movement always completes on its own — see §5 "pending".)

```bash
curl -s -X POST .../customers/$ID/balance/add \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-84421" \
  -d '{"amountGb": 5}'
```

This is what makes blind bot retries safe: a timeout on your side can be
retried with the same key without risking a double allocation.

## 4. Error model

Errors are always:

```json
{ "error": { "code": "INSUFFICIENT_POOL", "message": "…", "details": { } } }
```

| HTTP | code | meaning |
|---|---|---|
| 400 | `BAD_REQUEST` | malformed input (e.g. bad cursor, bad Idempotency-Key) |
| 401 | `UNAUTHORIZED` | missing/invalid/revoked API key |
| 403 | `RESELLER_SUSPENDED` | partner account frozen — contact Basil |
| 404 | `CUSTOMER_NOT_FOUND` | no such customer on your account |
| 409 | `INSUFFICIENT_POOL` | allocation exceeds your pool balance for that product |
| 409 | `INSUFFICIENT_STOCK` | Residential 2.0 only — we can't currently back that much new stock; contact Basil (never returned for allocations out of a pool you already own) |
| 409 | `CUSTOMER_NOT_ACTIVE` | operation not valid for the customer's status |
| 409 | `NOT_PROVISIONED` | customer has no proxy account yet (allocate first) |
| 409 | `KEY_LIMIT_REACHED` | already 2 active API keys |
| 409 | `IDEMPOTENCY_CONFLICT` | key reused with a different request |
| 409 | `REQUEST_IN_FLIGHT` | same key's first request still running |
| 409 | `PENDING_OPERATIONS` | in-flight balance moves must settle first (retry in ~1 min) |
| 422 | `VALIDATION_ERROR` | field-level validation failure (see `details`) |
| 429 | `RATE_LIMITED` | over your ceiling — honor `Retry-After` |
| 502 | `UPSTREAM_ERROR` / `ALLOCATION_REJECTED` / `RECLAIM_FAILED` | upstream proxy network refused — usually transient, retry |
| 502 | `STOCK_UNVERIFIABLE` | Residential 2.0 only — stock couldn't be verified, so the request was refused rather than guessed at; transient, retry |
| 503 | `SYSTEM_BUSY` | brief contention on the balance mutation lock — retry in a few seconds |
| 503 | `RESELLER_NOT_CONFIGURED` | partner program disabled server-side |

## 5. The pool model (how your GB move)

<!-- pool1:start -->

* **One pool per product.** Residential GB and Residential 2.0 GB are separate
  balances that never convert into each other. `GET /me` returns both under
  `pools[]`.

<!-- pool1:end -->

<!-- res2only:start -->

* **Your pool is one balance of GB.** `GET /me` returns it under `pools[]` —
  read the entry whose `product` is `residential2` and ignore any legacy
  compatibility blocks beside it.

<!-- res2only:end -->

<!-- pool1:start -->

* You buy GB wholesale (bank transfer); Basil records the sale and your **pool
  balance** goes up. A purchase can briefly show as *pending* — recorded but
  not yet physically confirmed; pending GB are visible but not spendable.
  (Residential 2.0 purchases are never pending: they either complete or are
  refused outright.)

<!-- pool1:end -->

<!-- res2only:start -->

* You buy GB wholesale (bank transfer); Basil records the sale and your **pool
  balance** goes up — a purchase either completes immediately or is refused
  outright, nothing sits pending. (The API still carries the `PENDING` status
  and pending-purchase fields for compatibility; on your account they are
  always empty.)

<!-- res2only:end -->

* `balance/add` moves GB **pool → customer**; `balance/remove` moves GB
  **customer → pool**. Suspend/kill/delete reclaim the customer's whole
  balance back to your pool automatically.
* Some mutations return `"balanceStatus": "pending"` (or reclaim
  `"status": "pending"`): the operation is **committed** and the physical
  move is being retried — it lands within minutes, no action needed. Committed
  in-flight credits show up as `pendingAllocationMb` on the customer read.
* **Once GB are in your pool they are yours.** Allocating them to a customer,
  reclaiming them, or moving them between customers is never refused for
  stock reasons on either product — only `INSUFFICIENT_POOL` (you tried to
  spend more than you hold) can stop it.
<!-- pool1:start -->

* **One customer, up to two accounts.** A customer row is shared across
  products; the upstream account for a product is created lazily on that
  product's first allocation. `usernames` on every customer object tells you
  which ones exist (`null` = not provisioned yet), and `products` lists them.

<!-- pool1:end -->

<!-- res2only:start -->

* **Accounts are created lazily.** A customer's proxy account appears on their
  first allocation. `usernames` on every customer object tells you what exists
  (`null` = not provisioned yet), and `products` lists their products.

<!-- res2only:end -->


---

## 6. Endpoints

### 6.1 Account

`GET /me` → your account, pool balances (incl. pending purchases), customer
counts, and active API keys (the one making the call is flagged `current`).

<!-- pool1:start -->

```json
{ "reseller": { "…": "…" },
  "pool":  { "product": "residential", "balanceMb": 412000, "balanceGb": 412,
              "pendingSaleMb": 0, "pendingSaleCount": 0 },
  "pools": [ { "product": "residential",  "balanceMb": 412000, "…": "…" },
             { "product": "residential2", "balanceMb": 95000,  "…": "…" } ],
  "customers": { "total": 61, "active": 58 },
  "keys": [ { "…": "…" } ] }
```

`pool` is the residential block, kept for v1.0 clients; read `pools` instead.

<!-- pool1:end -->

<!-- res2only:start -->

```json
{ "reseller": { "…": "…" },
  "pool":  { "…": "…" },
  "pools": [ { "…": "…" },
             { "product": "residential2", "balanceMb": 95000, "balanceGb": 95,
               "pendingSaleMb": 0, "pendingSaleCount": 0 } ],
  "customers": { "total": 12, "active": 11 },
  "keys": [ { "…": "…" } ] }
```

`pool` is a legacy v1.0 compatibility block — ignore it and read the
`residential2` entry in `pools`.

<!-- res2only:end -->


`GET /ledger?limit=25&cursor=…&product=…` → newest-first pool history. Every
entry is a signed MB delta and carries its `product`: `SALE` (+, your
purchases), `ALLOCATE` (−, to customers), `RECLAIM` (+, back from customers),
`ADJUST` (± corrections). Without `product` you get **every** product's rows.
Pass back `nextCursor` until it is `null`.

`GET /sales?limit=25&cursor=…&status=PENDING|COMPLETED|FAILED&product=…` →
your purchase history, newest first: `{ id, product, amountMb, amountGb,
priceEurMinor, eurPerGb, bankReference, status, createdAt, completedAt }`.
Without `product` you get every product's purchases.
`PENDING` purchases are recorded but not yet physically confirmed (§5) —
visible, not spendable; a `FAILED` row never credited your pool (contact
Basil). Same cursor pagination as `/ledger`.

### 6.2 Customers

Each end customer = one proxy account (username + password) that lives
forever. **Top up the same customer for repeat purchases — never create a new
customer per order** (see §8).

`POST /customers` — body `{ displayName?, initialMb? | initialGb?, product? }`
* Without `initialMb`: creates the customer record only; no proxy account
  exists yet (`balanceStatus: "unprovisioned"`, `credentials: null`).
* With `initialMb`: provisions the proxy account **for `product`**, allocates
  from that product's pool, and returns ready-to-use `credentials` — one call
  from sale to working proxy. `201` with `balanceStatus: "active"` (or
  `"pending"`, see §5).
* On Residential 2.0 those credentials need **about a minute** before the
  network accepts them (`warmupSeconds` in the response says how long) — a
  407 in that window is the gateway catching up, not a bad password. Don't
  hand them straight to a customer who will test them instantly.
* If the pool can't cover `initialMb`, you get `409 INSUFFICIENT_POOL` and
  **no customer is created**.
* If the allocation fails for any other reason after the customer was
  created, the response is still `201` with `balanceStatus: "failed"` and an
  `allocationError` — the customer exists; retry with `balance/add`, do
  **not** re-POST (that creates a second customer).

`GET /customers?limit&cursor&q&status&product` — list/search (`q` matches
display name or either proxy username; `status` = `ACTIVE|SUSPENDED|DELETED`).
The customer rows are the same whatever `product` you pass — it only picks
which product's daily snapshot rides along in `lastSnapshot`.

`GET /customers/:id?product=…` — customer + that product's balance:

<!-- pool1:start -->

```json
{
  "customer": { "id": "…", "displayName": "…", "status": "ACTIVE",
                 "proxyUsername": "ck_x8f3k2",
                 "usernames": { "residential": "ck_x8f3k2",
                                "residential2": "ck_9m2xq7t" },
                 "products": ["residential", "residential2"],
                 "createdAt": "…" },
  "product": "residential",
  "balance": { "balanceMb": 4820, "source": "cache", "ageSeconds": 12,
                "pendingAllocationMb": 0 }
}
```

<!-- pool1:end -->

<!-- res2only:start -->

```json
{
  "customer": { "id": "…", "displayName": "…", "status": "ACTIVE",
                 "usernames": { "…": null,
                                "residential2": "ck_9m2xq7t" },
                 "products": ["residential2"],
                 "createdAt": "…" },
  "product": "residential2",
  "balance": { "balanceMb": 4820, "source": "cache", "ageSeconds": 12,
                "pendingAllocationMb": 0 }
}
```

(The object also carries legacy compatibility fields — ignore anything not
shown here.)

<!-- res2only:end -->


`source` is `cache` (default, ≤ ~45 s old), `live` (`?fresh=true` — budgeted,
see §2), or `none` (not provisioned for that product yet).

### 6.3 Balance

`POST /customers/:id/balance/add` — body `{ amountMb | amountGb, product? }` →
`{ product, movedMb, balanceStatus, newBalanceMb, credentials }`. The
first-ever add **for that product** provisions its proxy account and returns
its credentials (see the warm-up note in §6.2 for Residential 2.0).

<!-- pool1:start -->
For Residential 2.0, a top-up always adds its full amount to the customer's
balance. If usage had run past the allocation (the cap is enforced with a
short lag, so a busy customer can end a little over it), the excess is written
off automatically before the add. It shows in `GET /ledger` as a `SYSTEM`
`ADJUST` credit plus a matching `ALLOCATE` debit — net zero to your pool.
<!-- pool1:end -->
<!-- res2only:start -->
A top-up always adds its full amount to the customer's balance. If usage had
run past the allocation (the cap is enforced with a short lag, so a busy
customer can end a little over it), the excess is written off automatically
before the add. It shows in `GET /ledger` as a `SYSTEM` `ADJUST` credit plus
a matching `ALLOCATE` debit — net zero to your pool.
<!-- res2only:end -->

`POST /customers/:id/balance/remove` — body
`{ amountMb | amountGb, product? }` where `amountMb` may be `"all"` →
`{ product, requestedMb, movedMb, status, newBalanceMb }`. Removal is capped
at the customer's live balance for that product; `movedMb` says what actually
moved.

### 6.4 Lifecycle

| call | effect |
|---|---|
| `POST /customers/:id/suspend` | freeze + reclaim all GB to your pools. Access stops as the balance hits zero. |
| `POST /customers/:id/kill` | suspend **plus** immediate password rotation. Use this — not a bare `rotate-credentials` — when you need someone actually stopped: it is the reclaim to a zero balance that locks the account, and rotation alone is not a clean tear-down (§7). For abuse. |
| `DELETE /customers/:id` | kill + status `DELETED`. The record stays (proxy accounts are permanent upstream) and can be reactivated. |
| `POST /customers/:id/reactivate` | back to `ACTIVE` with **freshly rotated credentials** in the response (previous ones are dead). Works from `SUSPENDED` and `DELETED`. |

<!-- pool1:start -->

**Lifecycle calls take no `product` — they cover every product the customer
holds.** Responses report each leg separately: `rotated` + `reclaim` for
residential, `rotatedRes2` + `reclaimRes2` for Residential 2.0 (`null` when
they hold no such account). The legs are independent, so one can succeed while
the other retries. A reclaim summary is `completed` / `pending` / `noop` /
`failed` + `movedMb`; a `pending` reclaim self-heals, a `failed` one means
retry the call. `reactivate` likewise returns `credentials` and
`res2Credentials`.

<!-- pool1:end -->

<!-- res2only:start -->

**Lifecycle calls take no `product` — they cover everything the customer
holds.** Read the Residential 2.0 leg of the response: `rotatedRes2` +
`reclaimRes2` (the bare `rotated`/`reclaim` fields are legacy compatibility
legs — ignore them). A reclaim summary is `completed` / `pending` / `noop` /
`failed` + `movedMb`; a `pending` reclaim self-heals, a `failed` one means
retry the call. `reactivate` likewise returns fresh credentials under
`res2Credentials`.

<!-- res2only:end -->


Two ordering guards you may hit: `DELETE` answers `409 PENDING_OPERATIONS`
while an allocation credit is still settling (the customer stays suspended —
retry in a minute), and `reactivate` answers the same while a reclaim is still
settling. Both clear on their own.

### 6.5 Credentials & usage

`GET /customers/:id/credentials?product=…` → that product's current
username/password + ready proxy URLs. `POST /customers/:id/rotate-credentials?product=…`
→ rotate + return fresh ones (old password dies immediately). Both answer
`409 NOT_PROVISIONED` if the customer has no account for that product yet.

<!-- pool1:start -->

```json
{ "product": "residential",
  "credentials": {
    "username": "ck_x8f3k2", "password": "AbCdEf…",
    "host": "residential.basilproxies.com",
    "ports": { "http": 1000, "socks5": 1002 },
    "urls": { "http": "http://ck_x8f3k2:AbCdEf…@residential.basilproxies.com:1000",
               "socks5": "socks5://ck_x8f3k2:AbCdEf…@residential.basilproxies.com:1002" } } }
```

<!-- pool1:end -->

```json
{ "product": "residential2",
  "credentials": {
    "username": "ck_9m2xq7t", "password": "3f1c…-…",
    "host": "eu-residential2.basilproxies.com",
    "hosts": { "eu": "eu-residential2.basilproxies.com",
                "us": "us-residential2.basilproxies.com",
                "ap": "ap-residential2.basilproxies.com" },
    "ports": { "rotating": 4242, "sticky": 4243 },
    "urls": { "rotating": "http://ck_9m2xq7t:3f1c…@eu-residential2.basilproxies.com:4242",
               "sticky":  "http://ck_9m2xq7t:3f1c…@eu-residential2.basilproxies.com:4243" },
    "warmupSeconds": 60 } }
```

<!-- pool1:start -->

Note the different shape: Residential 2.0 has **two HTTP ports and no SOCKS5
tier**, and carries `hosts` (§7.1) plus `warmupSeconds` (§6.2). Branch on the
field names, not on the product string, if you render these generically.

<!-- pool1:end -->

<!-- res2only:start -->

Residential 2.0 has **two HTTP ports and no SOCKS5 tier**, and carries `hosts`
(§7.1) plus `warmupSeconds` (§6.2).

<!-- res2only:end -->


`GET /customers/:id/usage?duration=24h|7d|30d|90d&granularity=hour|day&product=…`
→ `{ product, duration, granularity, points: [{ date, usedMb }], totalMb }`.

<!-- pool1:start -->

* `residential` reads the live proxy network (~60 s cache) and honours
  `granularity`. Uncached combinations have their own budget (default
  **20/min**) — repeat reads of the same customer/duration serve from cache
  and are effectively free.

<!-- pool1:end -->

* `residential2` is served from our daily snapshots and **always answers
  `granularity: "day"`**, whatever you ask for — there is no hourly series for
  it. `date` is then a plain `YYYY-MM-DD` day. Always read the granularity off
  the RESPONSE before formatting labels.

`GET /usage?days=30&product=…` → your whole book for one product, daily:
`{ product, days: [{ day, usedMb, balanceMb, customers }], totalUsedMb }` —
built from daily snapshots, perfect for a dashboard chart.

### 6.6 Targeting catalogue

<!-- pool1:start -->

`GET /proxy-settings` → `{ countries, cities, regions, isp }`, cached a few
hours. This is the same country/city list app.basilproxies.com itself offers
retail customers — nothing broader. Regions and ISP targeting suffixes work
(§7.2), but there's currently no browsable list for them (same gap as retail).

This catalogue describes the **residential** pool. Residential 2.0 takes
ISO-3166 alpha-2 country codes on the same footing (§7.1); there is no
published city/ASN catalogue for it yet — ask us for the codes you need.

<!-- pool1:end -->

<!-- res2only:start -->

Residential 2.0 takes ISO-3166 alpha-2 country codes directly on the username
(§7.1) — no catalogue call needed for countries. There is no published
city/ASN catalogue yet; ask us for the codes you need and skip
`GET /proxy-settings` (it describes a legacy catalogue that does not apply to
your product).

<!-- res2only:end -->


---

## 7. Using the proxies (for your end customers)

### 7.1 Residential 2.0

Endpoints — **HTTP only, no SOCKS5 tier on this product**:

```
Rotating  eu-residential2.basilproxies.com:4242     new IP per request
Sticky    eu-residential2.basilproxies.com:4243     hold one IP
```

**Regional entry points.** One pool, one credential set, three doors into it —
swap the host prefix for whichever is nearest your customer. This is a latency
choice only; it does not constrain the exit country, and the same username and
password work on all three.

```
eu-residential2.basilproxies.com     Europe, Africa, Middle East (default)
us-residential2.basilproxies.com     Americas
ap-residential2.basilproxies.com     Asia-Pacific
```

The `credentials` response carries all three under `hosts` (§6.5) — read them
from there rather than hardcoding, and hand each of your customers the nearest
one. A customer in Sydney routed through the EU door will report the product as
slow when nothing is wrong with it.

Targeting is encoded as **hyphen-prefixed tokens on the USERNAME**. The
password is always sent as-is.

<!-- pool1:start -->

(That is the opposite of classic residential, §7.2, where targeting rides the
password — don't mix the two schemes up when you run both.)

<!-- pool1:end -->


```
ck_9m2xq7t                                       rotating, worldwide
ck_9m2xq7t-country-us                            country (ISO-3166 alpha-2, lowercase)
ck_9m2xq7t-country-us-city-newyork               city (never combine city and ASN)
ck_9m2xq7t-country-us-asn-7922                   ASN
ck_9m2xq7t-country-us-type-residential-os-windows   OS (needs the pool type spelled out first)
ck_9m2xq7t-country-us-session-a1b2c3d4-lifetime-15  sticky: session id + minutes
```

* Sticky needs **both** `-session-` and `-lifetime-`, and only works on the
  sticky port (4243). `lifetime` is in minutes, 1–1440 (24 h ceiling).
* City and ASN cannot be combined — pick one.
* Order matters: country → city|asn → type/os → session → lifetime.

Example: `curl -x http://ck_9m2xq7t-country-de:PASS@eu-residential2.basilproxies.com:4242 https://ipinfo.io`

Fresh credentials are refused (`407`) for roughly the first minute after they
are issued — see `warmupSeconds` in §6.5. It can occasionally take longer;
treat `warmupSeconds` as a typical case, not a guarantee.

**How fast a kill actually bites** (measured against the live gateway, so you
can promise your own customers the right thing): rotating the password refuses
new connections within about a second, but it is not a clean tear-down — a
transfer already in flight runs to completion, and the retired password has
been seen authenticating intermittently for up to half a minute afterwards.
What reliably locks the account is the **reclaim to a zero balance** that
`kill` performs alongside the rotation: that lands upstream in roughly fifteen
seconds and stays. For abuse handling, always use `kill`.

<!-- pool1:start -->

### 7.2 Residential

Endpoints:

```
HTTP    residential.basilproxies.com:1000
SOCKS5  residential.basilproxies.com:1002
```

Geo/session targeting is encoded as **suffixes on the password**, in this
order: country → session → lifetime.

```
password                                  rotating, worldwide
password_country-US                       rotating, US only
password_country-US_session-a1b2c3d4      sticky session (8-char id you choose)
password_country-US_session-a1b2c3d4_lifetime-10   sticky, 10-min lifetime (1–120, default 30)
password_city-new.york                    city targeting (dots for spaces)
password_region-western.cape              region targeting
password_isp-att                          ISP targeting
```

Example: `curl -x http://ck_x8f3k2:AbCdEf_country-DE@residential.basilproxies.com:1000 https://ipinfo.io`

Sticky sessions pin one exit IP for the lifetime as long as the session id
stays the same; new id = new IP. Rotating your customer's password (kill /
rotate-credentials) invalidates the credentials immediately, so their next
connection is refused — but reach for `kill` rather than a bare rotation when
you need someone stopped, because `kill` also reclaims their balance to zero.

<!-- pool1:end -->

## 8. Rules of the road (AUP flow-down)

* **One end customer = one proxy account, forever.** Top it up on repeat
  purchases; never create a fresh customer per order. (Proxy accounts cannot
  be deleted once created — churning them is a compliance flag and will get
  throttled.)
* **SMTP is blocked network-wide** (ports 25/465/587 outbound). Mail-sending
  bots will not work through residential proxies; IMAP/POP receiving works.
* **Residential 2.0 stock is bought ahead in blocks.** Buying more of it can
  therefore be refused (`409 INSUFFICIENT_STOCK`) when we can't back it yet —
  give us a heads-up before a big drop rather than discovering it at checkout.
  GB already in your pool are never affected (§5).
* Illegal traffic, fraud, and CSAM are kill-on-sight — the kill switch (§6.4)
  exists for your own moderation too.
* Keep your customers on your own brand. They only ever need the proxy host
  and the credentials you hand them (§7) — never the partner portal or
  dashboard. Want them on your own hostname instead? Ask us about CNAME
  white-labeling.

<!-- pool1:start -->

## 9. Changelog

* **2026-09-07** — Residential 2.0 top-ups write off any upstream overrun first, so a top-up always adds its full amount (§6.3).
* **v1.2 (2026-08)** — **Residential 2.0** as a second product. Every money,
  credential and usage endpoint takes an optional `product`
  (`residential` | `residential2`), defaulting to `residential` — existing
  integrations are unaffected. `GET /me` gains `pools[]`; customers gain
  `usernames` + `products`; lifecycle responses gain `rotatedRes2` /
  `reclaimRes2`; `/ledger`, `/sales`, `/usage`, `/customers` gain a `product`
  filter. New errors `INSUFFICIENT_STOCK` (409) and `STOCK_UNVERIFIABLE`
  (502) apply to buying Residential 2.0 stock only, never to spending your own
  pool. Its credentials carry `hosts` — three regional gateway entry points
  into one pool (§7.1). See §7.1 for its endpoints and username-token
  targeting.
* **v1.1 (2026-07)** — `GET /sales` (purchase history); the partner portal at
  https://partner.basilproxies.com (sign in by pasting an API key — the portal
  is a UI over this same API, including self-service key rotation).
* **v1 (2026-07)** — initial release: account/ledger, customer lifecycle,
  balance add/remove, usage, credentials, targeting catalogue, idempotent
  mutations, per-partner rate limits.

<!-- pool1:end -->

