# CatProxies Reseller API

Documentation for the CatProxies **Reseller API**: the wholesale API for partners who
resell our proxies to their own customers, at the reseller prices set for their account.

This document is the complete reference. It is generated from the same source as the
human page at https://catproxies.com/docs, so the two always agree.

- Base URL: `https://catproxies.com/api/v1/public`
- Auth: `Authorization: Bearer cp_your_api_key_here`
- Keys are created at https://catproxies.com/reseller/api
- Rate limit: 1000 requests per hour per key.

**If you are an ordinary customer rather than a reseller, you want the User API
instead: https://catproxies.com/docs/api/llms.txt** That one bills at normal retail
prices and needs no reseller agreement.

## How the reseller model works

You sell from the catalogue assigned to your account, not from an open pool. Your
`/store` lists exactly the packages an admin has enabled for you, each at the reseller
price we set for you. Every order is paid from your prepaid balance.

So the whole API surface is: read your store, buy a package, get credentials, then
manage and extend. There is nothing to provision or configure beyond that.

You can also tag each order with the end customer you resold it to, which is what the
customer endpoints are for. That is purely our-side metadata and is never sent upstream.

## Authentication

Send the key as a bearer token on every request:

```
Authorization: Bearer cp_your_api_key_here
```

- Keys start with `cp_`. Up to 5 per reseller account.
- A key can spend your balance. Treat it like a password.
- Keys are per reseller and are rejected on the customer-facing User API.

## Rate limits

1000 requests per hour per key, on a rolling window. Every response carries:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per hour |
| `X-RateLimit-Remaining` | How many are left |
| `X-RateLimit-Reset` | When capacity returns (ISO 8601) |

Going over returns `429` with `Retry-After`. `GET /account` reports the same figures
in `rateLimits`. The mobile targeting endpoints share a tighter limit of 60 requests
per 10 minutes, so cache those lists.

## Products

Every product in `/store` has a `proxyType`. Use that exact value to filter the store
(`?proxyType=`) and to tell which product an order is.

| Product | proxyType | Sold by |
| --- | --- | --- |
| Standard Residential | `v2resi` | Bandwidth (GB) |
| Premium Residential | `resix` | Bandwidth (GB) |
| Custom ISP | `Isp` | Per IP, 1 day to 3 months, by city and optional carrier |
| Dedicated ISP | `IspP` | Per IP, per month, pool based |
| Datacenter | `DatacenterP` | IPs plus a bandwidth pool |
| IPv6 | `Ipv6p` | Bandwidth or unlimited |
| Rotating Mobile | `RotatingMobile` | Bandwidth (GB) |

## Quick start

```bash
KEY="cp_your_api_key_here"
API="https://catproxies.com/api/v1/public"

# 1. See what you can sell, and your price
curl -s -H "Authorization: Bearer $KEY" "$API/store"

# 2. Buy a package (paid from your balance)
curl -s -X POST "$API/order" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID_FROM_STORE" }'

# 3. Read the credentials back
curl -s -H "Authorization: Bearer $KEY" "$API/order/ORDER_ID"
```

### Buying is not idempotent (important)

A purchase sets up a live plan and charges your balance. If a call times out or the
outcome is unclear, look the order up with `GET /orders` before retrying, or you may
pay for two.

## Errors

Errors are JSON with a readable `message` and `"type": "error"`.

| Status | Meaning | What to do |
| --- | --- | --- |
| 400 | The request was wrong | Read `message`. Usually a missing or invalid field. |
| 401 | Bad or inactive key | Check the key and that it is still active. |
| 400 | Insufficient balance | The reseller API returns this as a 400 with message "Insufficient balance", not a 402. Top up your reseller balance. |
| 403 | Profile or country | POST /order also answers 403 for "Please add a name and address to complete your profile", for a blocked billing country, and for a product not available to your account. None of these are retryable. |
| 404 | Order not found OR unsupported | The whitelist endpoints answer 404 with "Order not found or does not support IP whitelisting" for a product that has no whitelist, not only for a missing order. |
| 404 | Not found | The order or package id does not exist on your account. |
| 409 | Conflict | Read the message. On `POST /order/custom` the charge stands and a human is finishing the order: do NOT retry. On a Datacenter, IPv6 or Custom ISP renewal another renewal of the same order is still running, or an earlier one is under review: that call was not charged; wait, or contact us if it is under review. |
| 429 | Rate limited | Wait for `Retry-After`, then continue. |
| 500 or 502 | Our side failed | Safe to retry a read. Do NOT blindly retry an order or a renewal: read the message first. On `POST /order/:id/extend`, "Failed to extend proxy plan. Your balance has been refunded." means nothing happened (retry later), but "Your renewal payment went through, but it needs manual review..." means you were charged and our team is finishing it (do NOT retry). |

## Product guides

Buying is the same call for every product. What differs is the extra body a purchase needs, the host and port you connect to, and where the targeting flags go.

Hosts and per-IP lines come from `GET /order/:id` and `GET /order/:id/proxies`. All of them are plain unbranded IPs, safe to pass straight to your own customers. On Custom ISP you can also use your own hostname, with the same Auto option we use (see Custom ISP).

### What can be renewed

| Product | Renewable | How |
| --- | --- | --- |
| Custom ISP | Yes | `POST /order/:id/extend` with `{}` for the same length, or with a `packageId` from `extendOptions` for another length (for example a 1-day plan for 1 month). Same IPs. Replacements are not renewed. |
| Dedicated ISP | Yes | `POST /order/:id/extend` with `{}`. Another fixed 30 days at `resellerPrice` × the order IP count. |
| Standard Residential | Yes | Adds bandwidth. Pick a bandwidth pack from `GET /order/:id/extendOptions` and send its `packageId` to `POST /order/:id/extend`. |
| Premium Residential | Yes | Exactly like Standard Residential, with no extra body parameters. |
| Datacenter | Yes | `POST /order/:id/extend` with `{}`. Resets the data to the plan's full GB; your remaining days carry over. |
| IPv6 | Yes | `POST /order/:id/extend` with `{}`. Unlimited plans get days added, bandwidth plans get GB added (at most 5000 GB in total). |
| Rotating Mobile | Yes | With just the `packageId`, like buying it. |

### Custom ISP (`Isp`)

#### What it is

Custom ISP (`proxyType: Isp`) is static ISP proxies where your customer picks everything: the **tier** (Standard, or Clean Score with IPs that have a fraud score of 0), the **plan length** (1 day, 7 days, 1 month, 2 months or 3 months), the **city**, and optionally the **carrier** (ASN) and **UDP** support. Up to **5 IP replacements** can be bought with the plan. You pay `pricePerIp × quantity` + `carrierFeePerIp × quantity` (only with a carrier) + `replacements × replacementPrice`, from your balance, no VAT. You can buy it through the API or from the Store page in your reseller panel.

In the API the tier is `standard`, or `premium` which is sold as Clean Score: IPs checked right before delivery, with a fraud score of 0 when delivered; never check them on a score site through the proxy. The plan length is `1_day`, `7_days`, `1_month`, `2_months` or `3_months`.

#### Buying

**Complete Custom ISP workflow**

```bash
# 1. Your plans and prices, and the cities in stock.
#    tier=premium for Clean Score, udp=1 for UDP-capable IPs only.
curl "https://catproxies.com/api/v1/public/custom-isp/catalog?tier=standard&udp=0" \
  -H "Authorization: Bearer cp_your_api_key_here"
# -> plans.standard["1_month"] = { packageId, pricePerIp: 3.20, days: 30, minQty, maxQty, replacementPrice: 1.10 }
# -> carrierFeePerIp: 0.35, countries[].cities[].location = "United States - New York City"

# 2. Optional: carriers in that city (same tier and udp as you will buy)
curl "https://catproxies.com/api/v1/public/custom-isp/carriers?tier=standard&location=United%20States%20-%20New%20York%20City" \
  -H "Authorization: Bearer cp_your_api_key_here"
# -> carriers[] = { asn: 7922, name, stock }

# 3. Buy 5 IPs on that carrier, with 2 IP replacements
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "...", "ispData": { "quantity": 5, "location": "United States - New York City",
    "asn": 7922, "udp": false, "replacements": 2 } }'
# Charged: 3.20 × 5 + 0.35 × 5 + 1.10 × 2 = 19.95
```

| Field | Notes |
| --- | --- |
| `packageId` | From GET /custom-isp/catalog, plans[tier][length].packageId. One package is one tier and one length. Only tiers and lengths you have a reseller price for are listed. |
| `ispData.quantity` | Required. Number of IPs, within the plan minQty and maxQty. |
| `ispData.location` | Required. The city exactly as the catalog returns it, "Country - City". Or just the country (countries[].country, e.g. "United States") for IPs from any city in it: its stock is countries[].total, and it always uses any carrier (no asn). |
| `ispData.asn` | Optional. A carrier from GET /custom-isp/carriers for that city. Adds carrierFeePerIp per IP. |
| `ispData.udp` | Optional. true = only IPs with UDP support. Ask the catalog and carriers with udp=1 so the counts match. |
| `ispData.ip2locationIsp` | Optional, beta. true = you only get IPs that IP2Location (a well-known IP lookup database) marks as "ISP", meaning a home or fixed internet line, not business or hosting. Checked on every IP right before delivery, at no extra cost. Not every IP has this mark, so fewer IPs are available than the stock shows: for bigger orders use a big city or a whole country. On Clean Score it comes on top of the usual fraud score 0 check on IPQS. Default false. |
| `ispData.replacements` | Optional, 0 to 5. Each costs the plan replacementPrice. Used with POST /order/:id/replace. Not renewed. IP replacements can only be bought together with the order: they can't be added later, so choose how many you need when you buy. |
| `Tiers` | standard = "Standard". premium = "Clean Score": IPs checked right before delivery, with a fraud score of 0 when delivered. Checking an IP on a score site through the proxy raises its score, so check it from a connection that is not using the proxy. clean_score is true on these orders. |
| `Stock` | The catalog counts are at most a minute old. POST /order checks stock again before charging; a city or carrier that ran out is refused and nothing is charged. |
| `Delivery` | If no IP can be delivered on the spot, POST /order answers HTTP 202 with payload.settingUp true and payload.order (PENDING): you were charged and it retries by itself after 2, 7 and 12 minutes. Do not order again; follow it with GET /order/:id. If it still gets no IP it becomes CANCELED and the price is back on your balance automatically. Ordering the exact same choice again while one is still retrying cancels and refunds the earlier one. If the provider never answers clearly, the order is held for our team instead of refunded. |
| `Same choice paused` | If your last 3 orders for one exact choice (tier, location, asn, udp, IP2Location ISP check) all got no IP, that choice is refused for a while with HTTP 409 and payload.code CUSTOM_ISP_CHOICE_BLOCKED. Nothing is charged; payload.retryAfterMinutes says when it opens again. Pick another city or carrier meanwhile. |

#### Proxy format / connecting

Each line from `/order/:id/proxies` is one IP. All lines use the same port, **1337, for both HTTP and SOCKS5**. `host` is a plain server IP, or your own hostname once you set one up (below), never one of our names. The order has one password; each line's `login` carries its IP, so always build each proxy string from its own line. `ip` is the address the line exits from.

```bash
# Get the lines (one per IP). ?region= picks the connection server, default auto.
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/proxies?region=auto" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

```
<host>:<port>:<login>:<password>                   # line format
http://<login>:<password>@<host>:1337               # HTTP / HTTPS
socks5://<login>:<password>@<host>:1337             # SOCKS5, same port
```

| Field | Notes |
| --- | --- |
| `?region=` | Connection server for the lines; connection_regions lists them. Only the route changes, not the exit IP. auto (default): with your own hostname it is your hostname itself, which answers with the server closest to YOUR customer. Without one, it is the server IP closest to where each line's IP is. |
| `host` | A plain server IP, picked per order. Servers can change, so read the lines from the API each time instead of saving them. With your own hostname set up, host is that hostname (or <location>.<your hostname>), which always follows our server changes. |
| `GET /order/:id` | port "1337", auth_mode "logpas" and a note pointing to /proxies and /replace. `package_region` holds the pick as JSON: { kind: "customisp", tier, location, asn, udp, length, replacements, carrierName }. |

##### Your own hostname (white label with Auto)

Put the lines on a subdomain of your own domain, e.g. `isp.yourbrand.com`. We serve the same records we use: `isp.yourbrand.com` is Auto (it answers with the server closest to whoever connects) and `<location>.isp.yourbrand.com` is one location (`us-east`, `us-central`, `us-west`, `ca`, `uk`, `nl`, `de`, `fi`, `sg`, `jp`, `au`). Dead servers are taken out for you, like on ours. Nothing in the lines or in a DNS lookup of the A records points back to us. You can also do this from the API page of your reseller panel.

```bash
# 1. Add your hostname. The reply lists the 2 NS records to add.
curl -X POST "https://catproxies.com/api/v1/public/custom-isp/domain" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "isp.yourbrand.com" }'
# -> status "pending", records: [ { type: "NS", name: "isp.yourbrand.com", value: "kiki.bunny.net" },
#                                 { type: "NS", name: "isp.yourbrand.com", value: "coco.bunny.net" } ]

# 2. Add those 2 NS records at your DNS provider, then check. It switches on by itself once they are seen.
curl "https://catproxies.com/api/v1/public/custom-isp/domain" \
  -H "Authorization: Bearer cp_your_api_key_here"
# -> status "active", hosts: { auto: "isp.yourbrand.com", "us-east": "us-east.isp.yourbrand.com", ... }
```

| Field | Notes |
| --- | --- |
| `domain` | A subdomain of a domain you own (at least 3 parts, e.g. isp.yourbrand.com). One per account. Do not add A or CNAME records for it: the 2 NS records hand that name to us. |
| `status` | none, pending (waiting for your NS records; lines keep using plain server IPs) or active (lines use your hostname). GET checks again on every call while pending, at most every 20 seconds. DNS changes can take up to an hour. |
| `DELETE /custom-isp/domain` | Removes it. Your lines go back to plain server IPs right away; remove the NS records on your side too. |

#### Renewing

```bash
# Check the price of each length (one option per length, the plan's own first)
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/extendOptions" \
  -H "Authorization: Bearer cp_your_api_key_here"

# Renew for the plan's own length (empty body)
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'
# Charged: 3.20 × 5 + 0.35 × 5 = 17.75 (replacements are not renewed)

# Or renew for another length, e.g. a 1-day plan for 1 month: the id of that option
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID_FROM_EXTENDOPTIONS" }'
```

| Field | Notes |
| --- | --- |
| `Renewal` | POST /order/:id/extend with {} renews for the plan's own length. To renew for another length (1 day, 7 days, 1 month, 2 or 3 months), send the packageId of that option from GET /order/:id/extendOptions. Same IPs, at your price per IP for that length × IPs, plus carrierFeePerIp × IPs when the order has a carrier. One renewal at a time; an order under review cannot be renewed; if the result is unclear the payment is kept and our team checks it. If only some IPs could be renewed you still get 200: the order is flagged and our team completes the rest, so do not renew again. |

#### Good to know

**Please do not check the IPs through the proxy**

We check every IP before delivery. Visiting IP check or fraud score sites (IPQS and similar) through the proxy can raise the IP's fraud score, because those sites log every visit. To check an IP, look it up from another device or a connection that is not using the proxy, and ask your customers to do the same.

| Field | Notes |
| --- | --- |
| `IP locations` | Locations (country and city) are strictly based on the ipinfo.io database. Other IP databases can show a different city for the same IP. |
| `Carrier (ASN)` | A renewal keeps the same IPs. A replacement IP stays in the same city with the same UDP support, but for now it can come from a different carrier (ASN). |
| `Stock and delivery errors` | Sometimes a city or carrier runs out, or delivery fails. POST /order checks stock before charging, so most of these are refused with nothing charged. If delivery is refused after the charge, your balance is refunded straight away and the order is cancelled: buy again in a few minutes, or pick another city or carrier. If the result is unclear, we check whether the IPs were delivered and finish or refund the order within 24 hours. Do not order again in that case. |

```bash
# Swap one IP for a new one in the same city (uses one replacement)
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/replace" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "ip": "203.0.113.24" }'
```

| Field | Notes |
| --- | --- |
| `Replace IP` | POST /order/:id/replace { ip }. 200 { oldIp, newIp, left }. 202 { checking: true } = still being confirmed, do NOT retry. 400 when it is not a Custom ISP order or ip is missing. The new IP is in the same city with the same UDP support; the carrier can differ. |
| `Not available` | No reset-password, no IP whitelist and no stats endpoint on Custom ISP. |
| `Reseller panel` | Also sold on the Store page of your reseller panel, with the same prices and the same rules as the API. |

### Dedicated ISP (`IspP`)

#### What it is

Dedicated ISP (`proxyType: IspP`) is a **per-IP** product: order any quantity from **1 to 100 IPs** at a flat `resellerPrice` per IP (total = `resellerPrice × quantity`). It is **pool-based (no country to pick)**, the term is a **fixed 30 days**, and it is **username:password only**. Each IP is a dedicated line served from `GET /order/:id/proxies`.

#### Buying

**Complete Dedicated ISP workflow**

```bash
# 1. List Dedicated ISP packages - note resellerPrice (per IP). locations[] is
#    EMPTY (pool-based, no country). Any quantity 1-100 costs the same per IP.
curl "https://catproxies.com/api/v1/public/store?proxyType=IspP" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Buy 5 IPs - pass only the quantity.
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "...", "ispData": { "quantity": 5 } }'
# Charged: resellerPrice × 5. Term: a fixed 30 days.
```

| Field | Notes |
| --- | --- |
| `ispData.quantity` | Number of IPs, 1-100. Flat per-IP price across the whole range. Defaults to 1 if omitted - always send it explicitly. |
| `Term` | Fixed 30 days. Not selectable. |
| `Subnet plans` | A /24 block (isSubnet: true) is NOT buyable via the API (returns 400); arrange subnets with support. |
| `Delivery problems` | If the IPs cannot be delivered right away, the error says the order was flagged to our team. Your payment is kept and we retry automatically for up to 3 hours. If it still cannot be delivered, your balance is refunded automatically. If the result is unclear, our team delivers or refunds it within 24 hours. Never re-order: POST /order is not idempotent, so a retry is a new order and a new charge. |

#### Proxy format / connecting

Each line from `/order/:id/proxies` is one dedicated proxy with its own login/password. HTTP and SOCKS5 run on **different ports**: use `port_http` for HTTP/HTTPS and `port_socks` for SOCKS5 and UDP-over-SOCKS5. Always read both from the API rather than assuming they match. There is no gateway host and no username-appended targeting.

```bash
# Get the proxy lines (host / port / login / password per IP).
#    Normally ready immediately; if you get HTTP 202 { pending: true }, retry
#    in a few seconds.
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/proxies" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

```
http://<login>:<password>@<host>:<port_http>     # HTTP / HTTPS
socks5://<login>:<password>@<host>:<port_socks>  # SOCKS5 (UDP supported)
```

| Field | Notes |
| --- | --- |
| `Credentials` | Not on GET /order/:id. Fetch the host:port:login:password lines from GET /order/:id/proxies. |
| `Provisioning` | GET /order/:id/proxies may return HTTP 202 { pending: true, proxies: [] } for a few seconds after purchase; retry. stale: true = served from cache during a brief upstream blip, lines still valid. |

#### Renewing

```bash
# 1. Check the renewal cost (resellerPrice × 5 for another 30 days).
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/extendOptions" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Renew - packageId is resolved from the order, so send an empty body.
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'
# Charged: resellerPrice × 5 again, +30 days.
```

| Field | Notes |
| --- | --- |
| `Renewal` | POST /order/:id/extend with {}. Cost = resellerPrice × the order IP count, for another fixed 30 days. Not idempotent - serialize renew calls per order. |

#### Good to know

| Field | Notes |
| --- | --- |
| `IP locations` | Locations are strictly based on the ipinfo.io database. Other IP databases can show a different location for the same IP. |

### Standard Residential (`v2resi`)

#### What it is

Standard Residential (`proxyType: v2resi`) is a **bandwidth** product. Buy and extend it with just the `packageId` - no extra body fields. Geo and session targeting are **appended to the username**, so one set of credentials covers every country and session. Rotating IP per request by default. Username:password only: there is no IP whitelist and no usage stats on this product.

#### Proxy format / connecting

##### Connection ports

Use the host from `GET /order/:id` (`proxyCredentials.hostip`). Pick the port for the protocol and rotation you want.

| Protocol | Rotating port | Sticky port |
| --- | --- | --- |
| HTTP / HTTPS | `9000` | `10000` |
| SOCKS5 | `11000` | `12000` |

#### Targeting

##### Targeting - append to the username

Keep the password as-is and append flags to the **username**. `-type-residential` is **required**; the rest are optional.

| Target | Flag | Value format |
| --- | --- | --- |
| Type (required) | `-type-residential` | always include this |
| Country | `-country-us` | ISO-2 code, lowercase |
| State / region | `-state-california` | lowercase |
| City | `-city-losangeles` | lowercase, no spaces |
| ASN / carrier | `-asn-7922` | ASN number only |
| Sticky session | `-lifetime-30-session-ab12` | minutes (max 1440) + any id |
| UDP over SOCKS5 | `-requireUdp-true` | SOCKS5 only, use port 12000 |

Add `-lifetime-<minutes>-session-<id>` to hold the same exit IP (lifetime up to 1440 minutes = 24h); omit the sticky flags for a fresh IP on every request. For UDP traffic, connect over SOCKS5 on port `12000` and append `-requireUdp-true` to the username; this is optional and only needed for clients that use SOCKS5 UDP (leave it off for normal HTTP/S traffic). Discover valid countries, cities and ASNs with `GET /gresi-targeting-options?orderId=...` (see the Targeting endpoints).

**Example**

```bash
# Credentials from GET /order/:id
#   username: mtpeqbcevnemewc201120   password: jonxybfeuu
#   host:     HOST = proxyCredentials.hostip from GET /order/:id. It is a plain
#             unbranded IP on purpose, so your own customers never see where the
#             plan came from. HTTP 9000 rotating, 10000 sticky.

# Rotating IP in the US (new IP per request) - HTTP port 9000
curl -x HOST:9000 \
  -U "mtpeqbcevnemewc201120-type-residential-country-us:jonxybfeuu" \
  https://ipinfo.io

# Sticky IP in Los Angeles for 30 minutes - sticky HTTP port 10000
curl -x HOST:10000 \
  -U "mtpeqbcevnemewc201120-type-residential-country-us-state-california-city-losangeles-lifetime-30-session-ab12:jonxybfeuu" \
  https://ipinfo.io
```

| Feature | Notes |
| --- | --- |
| `Targeting reference` | GET /gresi-targeting-options?orderId=... returns the available countries, cities and ASNs for the order. |
| `City / state name lists` | The endpoint above returns the exact set for your order. For a ready-made list of city and US-state name tokens (same lowercase, no-separator format), download the Premium Residential targeting reference - its cities / us_states / us_state_cities lists apply here too. |

#### Renewing

| Feature | Notes |
| --- | --- |
| `Extending` | Adds bandwidth. GET /order/:id/extendOptions returns your bandwidth packs; POST /order/:id/extend with the chosen packageId. |

#### Good to know

| Feature | Notes |
| --- | --- |
| `Rotate password` | POST /order/:id/reset-password issues a new base password (old one stops immediately; new one activates in 1-2 minutes). The targeting flags live on the username, so only the password half of your proxy strings changes. |
| `Authentication` | Username:password only. There is no IP whitelist and no usage stats endpoint on Standard Residential. |

### Premium Residential (`resix`)

#### What it is

Premium Residential is a **bandwidth** product. Buy and extend it exactly like Standard Residential - no extra body parameters. Geo, session and optional quality filters are **appended to the password**, so a single set of credentials covers every country, session and mode. HTTP and SOCKS5 are both supported. The region is chosen by which endpoint you connect to.

#### Proxy format / connecting

##### Connection endpoints

`GET /order/:id` returns `proxyCredentials.hostip` / `port` (the Americas HTTP gateway IP by default) plus an `endpoints` object with the `ip:port` for every region and protocol. The host is always the IP from your order; pick the endpoint whose region matches your target country, on HTTP or SOCKS5, using the ports below.

| Region | HTTP port | SOCKS5 port |
| --- | --- | --- |
| Americas | `6011` | `6014` |
| Europe | `6012` | `6015` |
| Asia / Africa / Oceania | `6013` | `6016` |

##### Dedicated ISP pools

Besides the residential pool, every Premium Residential order can also use six fast, country-locked ISP pools with the same username and password and the same bandwidth. These are part of Premium Residential, not the separate Dedicated ISP product. `GET /order/:id` lists them in `proxyCredentials.isp_pools` (an array of `id`, `name`, `country`, `protocol` and `endpoint`). Each pool is its own `ip:port`, and the port decides the pool and its country.

| Pool (id) | Country | Port |
| --- | --- | --- |
| United States (`us`) | US | `6000` |
| United States Tickets (`us_tickets`) | US | `6001` |
| United Kingdom (`uk`) | GB | `6003` |
| Germany (`de`) | DE | `6006` |
| Germany DTAG (`de_dtag`) | DE | `6007` |
| Australia (`au`) | AU | `6002` |

**How the ISP pools differ**

- **HTTP only.** The pools have no SOCKS5 port.
- **No targeting.** Send the bare password (rotating) or `PASSWORD-session-a1b2c3d4_lifetime-1h` (sticky). Country, city, state, filters and device are not used by the pools, so leave them out.
- **Read the endpoint from `isp_pools`.** Don't hard-code it, as we may move a pool to a healthier server.

```bash
curl -x ISP_POOL_ENDPOINT -U "USERNAME:PASSWORD-session-a1b2c3d4_lifetime-1h" http://ip-api.com/json
```

#### Targeting

##### Targeting - append to the password

Keep the username as-is and append flags to the **password**. The **first** flag joins the password with a `-` and every flag after it with a `_`. All flags are optional, and a bare password returns a worldwide IP.

| Target | Flag | Value format |
| --- | --- | --- |
| Country | `country-us` | ISO-2 code, lowercase. Optional, omit for worldwide. |
| US state | `state-california` | state name, lowercase, no spaces (newyork). US only, not with a city. |
| City | `city-losangeles` | lowercase, no spaces or separators. Not with a state. |
| Sticky session | `session-a1b2c3d4` | exactly 8 lowercase letters/digits. One id = one IP. |
| Session lifetime | `lifetime-1h` | how long the sticky IP is held: minutes or hours (30m, 1h, 24h), up to 168h. |
| Quality filter | `streaming-1` | best-effort cleaner IPs. |
| Speed filter | `direct-1` | best-effort lower latency. |
| Auth filter | `skipispstatic-1` | best-effort fewer blocks. |
| Device | `device-windows` | windows, android, ios, macos or linux. Best-effort. Put it last. |

**How the flags combine**

- **First flag uses `-`, the rest use `_`** - e.g. `-country-gb_city-london_session-a1b2c3d4_lifetime-1h`.
- **City and state are mutually exclusive**, and both require a country. Use one or the other, never both.
- **Multi-word cities/states have no spaces or separators** - `city-losangeles`, `state-newyork`.
- **Rotating vs sticky.** No `_session-` = a fresh IP for every new connection (a connection your client keeps open keeps its IP, so open a new connection when you want a new IP). Add a unique 8-char `_session-<id>` (optionally `_lifetime-<up to 168h>`) to hold one IP; one line per unique id = a batch of different sticky IPs.
- The optional filters (`streaming-1`, `direct-1`, `skipispstatic-1`) are best-effort optimizations, not guarantees. Stacking several can lower availability - use only the one you need.
- **Device is best-effort too.** `_device-windows` asks for IPs from that type of device and most come back that way, but not all. It narrows the pool, so combining it with a city, a state or the filters (the speed filter the most) means more failed connections. Only the five listed values are accepted: any other value makes the connection hang with no error, so never pass a free-text value through. Put it after every other flag.

A sticky `session` id (exactly 8 lowercase letters/digits) maps to one exit IP, held on a best-effort basis for the `lifetime` you set (up to 168h). Omit the session for a fresh IP on every new connection; use a **unique id per proxy line** to get a batch of different sticky IPs.

##### Country, state & city lists

Country is an ISO-2 code (lowercase). Cities and US states are the plain name in lowercase with no spaces or separators (`losangeles`, `newyork`). The downloadable reference includes the **full country, city and US-state name lists** (the exact tokens our own dashboard uses) plus all ports and flag formats. Cities are grouped by country (`cities`), US states are in `us_states`, and US cities per state are in `us_state_cities`. A token can still return 503 if that place has no live IP at the moment - just broaden the targeting.

**Premium Residential targeting reference (JSON)**: 196 countries, full city & US-state name lists, all ports, and every flag format. https://catproxies.com/files/premium_residential_x_targeting.json

Tip: any city in the world can be requested, but because the residential pool is dynamic, very small cities may fall back to the wider region. Prefer larger cities. If a request returns no IP, broaden the targeting.

**Example**

```bash
# Credentials from GET /order/:id  (HOST = your order's hostip - a plain
# unbranded IP; per-region HTTP/SOCKS5 ports are in the endpoints object)
#   username: hftjvbtwgxxxceb205604
#   password: ngnstzylne
#   Americas HOST:6011   Europe HOST:6012   Asia HOST:6013

# Worldwide, rotating (bare password = a fresh IP on every new connection)
curl -x HOST:6011 -U "hftjvbtwgxxxceb205604:ngnstzylne" http://ip-api.com/json
# -> {"status":"success","country":"Brazil","city":"Sao Paulo","isp":"Example ISP","query":"203.0.113.5"}

# UK, city London, with the Quality filter (use the Europe endpoint)
curl -x HOST:6012 \
  -U "hftjvbtwgxxxceb205604:ngnstzylne-country-gb_city-london_streaming-1" \
  http://ip-api.com/json
# -> {"status":"success","country":"United Kingdom","city":"London","isp":"Example ISP","query":"198.51.100.7"}

# A DIFFERENT sticky IP per line: give each line its own unique 8-char session
curl -x HOST:6011 -U "hftjvbtwgxxxceb205604:ngnstzylne-country-us_session-a1b2c3d4_lifetime-1h" http://ip-api.com/json
curl -x HOST:6011 -U "hftjvbtwgxxxceb205604:ngnstzylne-country-us_session-e5f6g7h8_lifetime-1h" http://ip-api.com/json

# SOCKS5 works too - just use the region's SOCKS5 port
curl -x socks5h://HOST:6014 -U "hftjvbtwgxxxceb205604:ngnstzylne-country-us" http://ip-api.com/json

# Device targeting (best effort), always the LAST flag
curl -x HOST:6012 -U "hftjvbtwgxxxceb205604:ngnstzylne-country-de_session-a1b2c3d4_lifetime-1h_device-windows" http://ip-api.com/json

# Dedicated ISP pool: endpoint from isp_pools, HTTP only, no country or other targeting
curl -x ISP_POOL_ENDPOINT -U "hftjvbtwgxxxceb205604:ngnstzylne-session-a1b2c3d4_lifetime-1h" http://ip-api.com/json
```

#### Renewing

Extend it exactly like Standard Residential, with no extra body parameters. See Standard Residential, Renewing.

#### Good to know

##### Rotating the password

`POST /order/:id/reset-password` issues a brand-new password for the order (see the Endpoints section). Because targeting flags are appended to the **password**, a reset changes the **base** password only - rebuild your proxy strings with the new base password and re-append the same flags. The old password stops working immediately and the new one takes 1 to 2 minutes to activate.

### Datacenter (`DatacenterP`)

#### What it is

Datacenter plans (`proxyType: DatacenterP`) give you a list of **dedicated IPs** plus a **shared bandwidth pool** for a fixed duration - e.g. 100 IPs / 250 GB / 30 days. At purchase you decide how the IPs split across **US, CA, DE, GB and NL**. Every IP is its own proxy; they all share the same username:password. Each IP works two ways: **static** (that exact IP every time) or **rotating** (a new IP on every request) by adding a `-ttl-0` flag to the username.

#### Buying

**Complete Datacenter workflow**

```bash
# 1. List Datacenter packages - note ips (IP count), bandwidth (GB pool), days
curl "https://catproxies.com/api/v1/public/store?proxyType=DatacenterP" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Check live IP stock per country before allocating
curl "https://catproxies.com/api/v1/public/datacenterp-countries" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 3. Buy - country_proxies must total EXACTLY the package IP count
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "packageId": "...",
    "datacenterPData": {
      "country_proxies": { "US": 60, "DE": 20, "GB": 20 },
      "high_concurrency": false,
      "high_priority": false,
      "whitelisted_ips": false
    }
  }'
```

| Field | Notes |
| --- | --- |
| `country_proxies` | Must total exactly the package ips value. Allowed countries: US, CA, DE, GB, NL. Check /datacenterp-countries for live stock first - over-allocating a country fails the purchase. |
| `Addons` | high_concurrency (more parallel connections), high_priority (priority routing), whitelisted_ips (3 whitelist slots instead of 1). Each addon enabled adds +25% to the plan price (e.g. all three = +75%). All three are booleans and must be present in datacenterPData. |

#### Proxy format / connecting

```bash
# 1. Read the IP list back (ip_list is refreshed live on every call)
curl "https://catproxies.com/api/v1/public/order/ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Optional - whitelist your server IP for credential-less access
curl -X PATCH "https://catproxies.com/api/v1/public/order/ORDER_ID/whitelist" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "ip": "203.0.113.42" }'
```

**What GET /order/:id returns for Datacenter**

```json
// All IPs below are documentation examples. Real ones come from the API.
{
  "payload": {
    "order": {
      "id": "c5c7c6b5-e488-420c-b436-e90e86031751",
      "package_title": "Datacenter Proxies - 1 Month - 100 IPs - 250GB",
      "status": "CONFIRMED",
      "proxyCredentials": {
        "username": "dcuser482913",
        "password": "dcpass91827",
        "total_bandwidth": 250,
        "used_bandwidth": 12.4,
        "bandwidth_left": 237.6,
        "available_bandwidth": 237.6,
        "expired_at": "2026-08-04T10:00:00.000Z",
        "port_http": "1338",
        "port_socks5": "1339",
        "whitelist_ip": [],
        "ip_list": [
          {
            "iso2": "US",
            "name": "United States",
            "count_ips": 60,
            "cities": [
              { "name": "New York", "ips": ["203.0.113.24", "203.0.113.57", "..."] }
            ]
          },
          { "iso2": "DE", "name": "Germany", "count_ips": 20, "cities": ["..."] },
          { "iso2": "GB", "name": "United Kingdom", "count_ips": 20, "cities": ["..."] }
        ],
        "country_list": {
          "countries": { "US": 60, "DE": 20, "GB": 20 },
          "total": 100
        },
        // One rotating gateway per country this order has IPs in,
        // picked for THIS order. Read it from here every time; do not save it.
        "rotating_gateways": {
          "US": "198.51.100.10",
          "DE": "198.51.100.40",
          "GB": "198.51.100.50"
        },
        // Every usable gateway per country. The US has two.
        "rotating_gateway_sets": {
          "US": ["198.51.100.10", "198.51.100.20"],
          "DE": ["198.51.100.40"],
          "GB": ["198.51.100.50"]
        },
        "rotating_bound_ip_conflicts": []
      }
    }
  }
}
```

Ports: `1338` HTTP and HTTPS, `1339` SOCKS5.

**Using the proxies**

```bash
# Every IP in ip_list is a dedicated proxy. Same credentials on all of them.
#   HTTP:   <ip>:1338:<username>:<password>
#   SOCKS5: <ip>:1339:<username>:<password>

# STATIC - that exact IP every time
curl -x 203.0.113.24:1338 -U "dcuser482913:dcpass91827" https://ipinfo.io

# ROTATING - a new IP on every new connection: use rotating_gateways.<COUNTRY>
# from GET /order/:id as the host and append -ttl-0 to the USERNAME.
curl -x 198.51.100.10:1338 -U "dcuser482913-ttl-0:dcpass91827" https://ipinfo.io

# After whitelisting your server IP, credentials are optional (static only):
curl -x 203.0.113.24:1338 https://ipinfo.io
```

| Field | Notes |
| --- | --- |
| `Bandwidth` | One shared GB pool across all IPs in the plan. Track it via bandwidth_left on GET /order/:id (refreshed live). |
| `ip_list` | Grouped by country then city. Flatten cities[].ips to get every proxy IP. Refreshed live on each GET /order/:id. |
| `Authentication` | username:password on any IP, or whitelist your server IP (PATCH /order/:id/whitelist) and connect without credentials. |
| `Rotating IPs` | Use rotating_gateways.<COUNTRY> from GET /order/:id as the host and append -ttl-0 to the username for a new IP on every new connection, drawn from that order's own IPs in that country; omit the flag for a fixed IP. The gateway is picked per order, so read it from the API instead of hardcoding it. rotating_gateways only lists the countries this order has IPs in. Do not use one of your own ip_list IPs as the rotating host: with a whitelisted IP, a request through one of the order's own IPs is logged in by IP, the username is ignored and it always exits from that IP. |
| `Rotation granularity` | Per connection, not per request. Open a fresh connection for a new IP. |
| `US rotating` | The US has two gateways and each one only reaches about half of an order's US IPs. rotating_gateway_sets.US lists every usable US gateway for that order (normally two): point your own hostname's A-records at EVERY one of them to rotate through every US IP. |
| `rotating_bound_ip_conflicts` | Countries where the order owns the only rotating gateway. Rotating there keeps giving the same IP while an IP is whitelisted, so remove the whitelisted IP if your customer needs rotating in that country. Static IPs are not affected. |
| `Worldwide rotating` | One blended worldwide line is only possible for plans that include ALL five countries (US, CA, DE, GB, NL). Each gateway only accepts plans that own IPs in its country, so on a plan with fewer countries a worldwide host returns 407 whenever it lands on a country you did not buy. For such plans use one rotating_gateways line per purchased country. |

#### Renewing

```bash
# Renew before the end date: check the price, then renew (empty body)
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/extendOptions" \
  -H "Authorization: Bearer cp_your_api_key_here"

# Renew (empty body)
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'
```

| Field | Notes |
| --- | --- |
| `Renewing` | Supported. GET /order/:id/extendOptions returns one option: this order's own package at your resellerPrice × the add-on multiplier (+25% per add-on bought with the plan), which is exactly what the renewal charges (no VAT). POST /order/:id/extend with {} renews the same plan. Renewing resets your data to the plan's full GB for the new term. Any data you haven't used yet doesn't carry over. Your remaining days do: the new term is added on top of your current end date. One renewal per plan at a time. See Renewing a Datacenter or IPv6 plan below. |

##### Renewing a Datacenter or IPv6 plan

Three calls: check the price, renew, then read the order for the new end date. Datacenter and IPv6 always renew the order's own package, so there is nothing to choose. The price check charges nothing.

```bash
# 1. Check the price. Charges nothing. There is exactly one option:
#    packages[0].price is what step 2 takes from your balance (no VAT),
#    packages[0].note says what the renewal does to the data and days.
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/extendOptions" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Renew. Empty JSON body: the order's own package is used.
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'
# -> payload.extendOrderId, and payload.order with the new
#    proxyCredentials.expired_at already in it

# 3. Read the order again any time: proxyCredentials.expired_at is the
#    end date, bandwidth_left the data left.
curl "https://catproxies.com/api/v1/public/order/ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

- Renew before the end date. An expired plan cannot be renewed (403 "Cannot extend an expired proxy"); buy a new one instead.
- Datacenter: the price is your resellerPrice × the add-on multiplier (+25% for each add-on the plan was bought with). Renewing resets your data to the plan's full GB for the new term. Any data you haven't used yet doesn't carry over. Your remaining days do: the new term is added on top of your current end date.
- If step 2 times out or you are not sure it landed, do step 3 before trying again. A renewal that went through has moved expired_at. Sending it again would renew, and charge, a second time.
- Two 500 messages mean opposite things. "Failed to extend proxy plan. Your balance has been refunded." means nothing happened: try again later. "Your renewal payment went through, but it needs manual review by our team..." means you WERE charged and our team finishes it within 24 hours or refunds it: do not retry.
- One renewal per order at a time: a call that arrives while one is still running gets 409 and is not charged. A 409 "This plan's last renewal is being checked by our team" means an earlier renewal is under review: contact us, do not renew again.
- Related: POST /order/:id/reset-password rotates the password on both products (the flags live on the username, so only the password half of your proxy strings changes), and GET /order/:id/stats shows how the plan is being used.

#### Usage stats

```bash
# Usage stats: traffic over time, recent connections, top sites
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/stats" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

| Field | Notes |
| --- | --- |
| `Usage analytics` | GET /order/:id/stats: traffic and requests over time, recent connections and top sites. See Usage stats below. |

#### Good to know

| Field | Notes |
| --- | --- |
| `Rotate password` | POST /order/:id/reset-password issues a new 12-character password shared by every IP. It takes a few minutes to switch over on every location, and until then the old one may still work on some. Only the password changes - swap it into all your proxy strings. |
| `HTTP 429` | Returned by the proxy when you hit the plan’s concurrent-connection limit - lower parallelism, or buy with the high_concurrency addon for a higher limit. 407 = wrong credentials / non-whitelisted IP. |

### IPv6 (`Ipv6p`)

#### What it is

IPv6 plans (`proxyType: Ipv6p`) rotate through a huge pool of IPv6 addresses - a fresh exit IP on every request by default. Two plan flavors in /store: **bandwidth plans** (GB pool, no speed cap) and **unlimited plans** (bandwidth "0" = no GB cap, speed capped at the `speed` value in Mbps). Purchase needs only the `packageId` - no extra body fields.

| Field | Notes |
| --- | --- |
| `Bandwidth plans` | bandwidth > 0 in /store. Track usage via bandwidth_left / usage on GET /order/:id (refreshed live). |
| `Unlimited plans` | bandwidth "0" + speed cap in Mbps. usage is null in /order/:id - only expired_at limits the plan. |

#### Proxy format / connecting

##### Connection gateways

IPv6 is served through **country gateways** - the exit country is chosen by which gateway you connect to, and the same username:password works on all of them. Locations: **Worldwide, United States, Germany, United Kingdom, Netherlands**. Ports: `1338` HTTP/HTTPS, `1339` SOCKS5.

The gateway addresses are returned per order by `GET /order/:id` - unbranded plain IPs, safe to pass straight to your own customers:

```bash
curl "https://catproxies.com/api/v1/public/order/ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

**What GET /order/:id returns for IPv6**

```json
{
  "proxyCredentials": {
    "username": "...",
    "password": "...",
    "hostip": "<worldwide gateway>",   // default gateway
    "port": "1338",
    "ports": { "http": "1338", "socks5": "1339" },
    "endpoints": {                     // one gateway IP per country
      "ww": "...", "us": "...", "de": "...", "gb": "...", "nl": "..."
    },
    "usage": { "bandwidth_left": 87.4, ... }  // null on unlimited plans
  }
}
```

**The target site must support IPv6**

Exits are IPv6-only, so the destination needs an AAAA (IPv6) DNS record. Major platforms (Google, YouTube, Facebook, Instagram, TikTok) support it; many smaller sites do not and will be unreachable. Check your target domain first at [ready.chair6.net](https://ready.chair6.net/), then confirm the proxy works end-to-end by requesting `https://api6.ipify.org` through it. An unreachable IPv6-less target typically surfaces as a `503` from the proxy.

#### Targeting

##### Sessions - append to the username

Default is **rotating**: a new IPv6 address on every request, nothing to configure. For a **sticky session** (keep the same exit IP), append `-session-<id>-ttl-<seconds>` to the **username** (password unchanged). The session id must be a **number between 0 and 999999**; `ttl` is the hold time in **seconds**, up to **86400 (24h)** - e.g. `ttl-1800` = 30 min, `ttl-3600` = 1 hour. Use different session ids to hold several sticky IPs in parallel.

```bash
# Credentials + gateway IPs from GET /order/:id (same login on all gateways)
#   username: ipv6user291   password: v6pass817
#   endpoints.us -> US_GATEWAY,  endpoints.de -> DE_GATEWAY, ...
#   Those gateways are plain unbranded IPs on purpose, so your own customers
#   never see where the plan came from. HTTP 1338, SOCKS5 1339.

# Rotating - fresh IPv6 address on every request (US gateway)
curl -x US_GATEWAY:1338 -U "ipv6user291:v6pass817" https://api6.ipify.org

# Sticky - hold the same IPv6 exit for 60 minutes (ttl is in SECONDS: 60 min = 3600)
curl -x DE_GATEWAY:1338 \
  -U "ipv6user291-session-483920-ttl-3600:v6pass817" \
  https://api6.ipify.org

# SOCKS5
curl --socks5 US_GATEWAY:1339 -U "ipv6user291:v6pass817" https://api6.ipify.org
```

| Field | Notes |
| --- | --- |
| `Sticky session` | -session-<id>-ttl-<seconds> appended to the username. id numeric 0-999999; ttl is the hold time in SECONDS, max 86400 (24h) - e.g. 1800 = 30 min, 3600 = 1h. Omit both for rotating. |

#### Renewing

| Field | Notes |
| --- | --- |
| `Renewing` | Supported. GET /order/:id/extendOptions returns one option: this order's own package at your resellerPrice (no VAT). POST /order/:id/extend with {} renews the same plan. Unlimited plans get the package's days added on top of the current end date (always the plan's own length, for example 30 days on a 30-day plan). Bandwidth plans get its GB added on top of the data left, and the end date moves forward by the plan's own term. A bandwidth plan can hold at most 5000 GB in total: a renewal that would pass that is refused before anything is charged, and extendOptions returns no option. One renewal per plan at a time. See Renewing a Datacenter or IPv6 plan below. |

##### Renewing a Datacenter or IPv6 plan

Three calls: check the price, renew, then read the order for the new end date. Datacenter and IPv6 always renew the order's own package, so there is nothing to choose. The price check charges nothing.

```bash
# 1. Check the price. Charges nothing. There is exactly one option:
#    packages[0].price is what step 2 takes from your balance (no VAT),
#    packages[0].note says what the renewal does to the data and days.
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/extendOptions" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Renew. Empty JSON body: the order's own package is used.
curl -X POST "https://catproxies.com/api/v1/public/order/ORDER_ID/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'
# -> payload.extendOrderId, and payload.order with the new
#    proxyCredentials.expired_at already in it

# 3. Read the order again any time: proxyCredentials.expired_at is the
#    end date, bandwidth_left the data left.
curl "https://catproxies.com/api/v1/public/order/ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

- Renew before the end date. An expired plan cannot be renewed (403 "Cannot extend an expired proxy"); buy a new one instead.
- IPv6 unlimited plan: the package's days are added on top of the current end date. IPv6 bandwidth plan: the package's GB are added on top of the data left, and the end date moves forward by the plan's own term.
- If step 2 times out or you are not sure it landed, do step 3 before trying again. A renewal that went through has moved expired_at. Sending it again would renew, and charge, a second time.
- Two 500 messages mean opposite things. "Failed to extend proxy plan. Your balance has been refunded." means nothing happened: try again later. "Your renewal payment went through, but it needs manual review by our team..." means you WERE charged and our team finishes it within 24 hours or refunds it: do not retry.
- One renewal per order at a time: a call that arrives while one is still running gets 409 and is not charged. A 409 "This plan's last renewal is being checked by our team" means an earlier renewal is under review: contact us, do not renew again.
- Related: POST /order/:id/reset-password rotates the password on both products (the flags live on the username, so only the password half of your proxy strings changes), and GET /order/:id/stats shows how the plan is being used.

#### Usage stats

| Field | Notes |
| --- | --- |
| `Usage analytics` | GET /order/:id/stats: traffic and requests over time, recent connections and top sites. See Usage stats below. |

#### Good to know

| Field | Notes |
| --- | --- |
| `Authentication` | username:password only. IP whitelisting is currently unavailable for IPv6 plans. |
| `Rotate password` | POST /order/:id/reset-password issues a new 12-character password (old one stops immediately; new one activates in 1-2 minutes). Targeting flags live on the username, so only swap the new password into your proxy strings. |

##### Proxy response codes

| Code | Meaning |
| --- | --- |
| `407` | Wrong or missing proxy credentials. |
| `429` | Concurrent-connection / thread limit reached - lower parallelism or retry with backoff. |
| `503` | Could not reach the target - DNS failed or the site has no IPv6 (AAAA) support. |
| `504` | The target site did not respond in time. |

### Rotating Mobile (`RotatingMobile`)

#### What it is

Rotating Mobile (`proxyType: RotatingMobile`) is a **bandwidth** product on real mobile carriers. Buy and extend it with just the `packageId`. Location and session targeting are **appended to the username**. Rotating IP per request by default.

#### Proxy format / connecting

Use the host and port from `GET /order/:id` (`proxyCredentials.hostip` / `port`, HTTP port `5000`). The same credentials work for every location; you choose the location by the username flags below.

#### Targeting

| Target | Flag | Value format |
| --- | --- | --- |
| Country | `-country-US` | ISO-2 code |
| Region / state | `-region-california` | region code from /regions |
| City | `-city-los+angeles` | city code from /cities. Spaces are a PLUS sign. |
| Carrier / ISP | `-isp-at&t+wireless` | isp code from /isps |
| Sticky session | `-sid-ab12-ttl-30m` | any id + ttl (see below): one of 1m 5m 10m 30m 60m 120m 180m 240m |

For a sticky IP add `-sid-<id>-ttl-<duration>`, where duration is one of `1m 5m 10m 30m 60m 120m 180m 240m` (hold up to 4h). Omit the sticky flags for a fresh IP each request. All of `region`, `city`, `isp` and the sticky flags are optional.

**Value format: read this before targeting**

- Use the codes exactly as returned by the targeting endpoints. **Spaces are written as `+`, never as `-`**, e.g. `los+angeles`, `new+york`, `at&t+wireless`. Regions are full names (`california`), not 2-letter codes.
- An unknown or malformed value **fails with 502**, and it does not silently fall back to untargeted rotation. If you get a 502 on a targeted request, check the code first.
- Carrier availability varies by city, so a valid city + carrier pair can still return 502 if that carrier has no live IPs there right now. **For reliable carrier targeting, pin the carrier at country level** and omit the city flag. City targeting is metro-approximate, not pinpoint.
- Untargeted rotation is not evenly weighted across carriers. If you need a specific carrier mix, request each carrier explicitly rather than relying on the default rotation.

##### Discovering locations

The valid country / region / city / ISP codes come from the **Mobile Targeting endpoints** (`/mobile-targeting-options/*`). Those endpoints share a limit of 60 requests per 10 minutes - cache them (the lists rarely change), or download the full offline snapshot from the Targeting section for bulk setup. For a static starting point you can also reuse the city and US-state name lists in the **Premium Residential targeting reference** - the same place names apply, though the exact set valid for mobile comes from the endpoints above.

**Example**

```bash
# Credentials from GET /order/:id
#   username: mobileuser71   password: mob9182pass
#   host: HOST = proxyCredentials.hostip from GET /order/:id, a plain unbranded
#         IP on purpose so your own customers never see where the plan came
#         from.  port: 5000

# Rotating mobile IP in the US (new IP per request)
curl -x HOST:5000 \
  -U "mobileuser71-country-US:mob9182pass" \
  https://ipinfo.io

# Rotating IP on AT&T anywhere in the US
curl -x HOST:5000 \
  -U "mobileuser71-country-US-isp-at&t+wireless:mob9182pass" \
  https://ipinfo.io

# Sticky IP on AT&T in California for 30 minutes
curl -x HOST:5000 \
  -U "mobileuser71-country-US-region-california-isp-at&t+wireless-sid-ab12-ttl-30m:mob9182pass" \
  https://ipinfo.io
```

#### Renewing

Extend it with just the `packageId`, the same way you buy it.

#### Good to know

##### Resetting the login

`POST /order/:id/reset-password` gives the plan a **whole new login** and moves its remaining data onto it, so the **username and the password both change**. The response carries both (`username` and `password`); rebuild your proxy strings from them and re-append your targeting flags to the new username. The old login stops working straight away, and the plan's used and total GB carry over. Allowed once per hour per plan, not within 5 minutes of a top-up, and not on a plan with no data left.

### Usage stats

Datacenter and IPv6 orders come with usage analytics: traffic and requests over time, a log of recent connections, and the sites used most. It is one read-only endpoint, GET /order/:id/stats, and the view parameter picks which of the three you get. It never changes the plan, so you can pass the figures straight on to your own customers.

```bash
# Traffic over the last 24 hours, one row per hour (the defaults)
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/stats" \
  -H "Authorization: Bearer cp_your_api_key_here"

# The last week, one row per day
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/stats?timeframe=1week&interval=1day" \
  -H "Authorization: Bearer cp_your_api_key_here"

# Recent connections, 100 per page
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/stats?view=activity&page=1" \
  -H "Authorization: Bearer cp_your_api_key_here"

# The sites used most this month
curl "https://catproxies.com/api/v1/public/order/ORDER_ID/stats?view=top-domains&timeframe=1month" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

- timeframe: 1hour, 6hours, 1day (default), 1week, 1month or 3months. interval, for the overview only: 5min, 30min, 1hour (default), 6hour, 1day, 1week or 1month. An unknown value quietly falls back to the default, so check timeframe and interval in the reply.
- available: false means the stats source could not be reached just now and the lists are empty. Retry later; it does not mean zero traffic.
- The figures lag a little behind real time. For what is left on the plan right now, read proxyCredentials.bandwidth_left on GET /order/:id instead.
- Units: the overview is in GB, activity and top-domains traffic is in bytes, and success_rate is a percentage. Timestamps are "YYYY-MM-DD HH:MM:SS" strings.
- GET /order/:id/unlimited-metrics is the older name of the same endpoint and returns exactly the same thing.

## Endpoints

All paths are relative to `https://catproxies.com/api/v1/public`. Responses are JSON shaped as `{ type, message, payload, pagination? }`.

### Account

#### `GET /account`

Get your account balance, email, total orders, and rate limit information.

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `balance` | number | Current account balance in USD |
| `email` | string | Account email address |
| `displayName` | string | Account display name |
| `totalOrders` | number | Total number of confirmed orders |
| `rateLimits.requestsPerHour` | number | Maximum requests allowed per hour (1000) |
| `rateLimits.requestsUsed` | number | Number of requests made in current hour |
| `rateLimits.requestsRemaining` | number | Remaining requests in current hour |
| `rateLimits.resetAt` | string | When the rate limit counter resets (ISO 8601) |

Request:

```bash
curl -X GET https://catproxies.com/api/v1/public/account \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"balance": 187.05,
"email": "reseller@example.com",
"displayName": "John Doe",
"totalOrders": 42,
"rateLimits": {
  "requestsPerHour": 1000,
  "requestsUsed": 7,
  "requestsRemaining": 993,
  "resetAt": "2025-11-29T20:00:00.000Z"
}
  },
  "pagination": {}
}
```

#### `GET /billing`

Your billing statement, newest first: the same list as the Billing page in the reseller panel. Every purchase, renewal, GB added to a plan (POST /order/:id/bandwidth), deposit and refund on the account.

Read-only, charges nothing. direction "out" means money left your balance, "in" means it came back or was deposited. reference is the order ID the row belongs to (or the renewal ID when referenceType is "extend").

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number |
| `pageSize` | number | no | Items per page (max 100) |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `kind` | string | "purchase", "extend" (a renewal or GB added to a plan), "deposit" or "refund". |
| `label` | string | What the row is, e.g. "Standard Residential V2 5GB", "Added 10 GB", "Extend 30 days". |
| `proxyType` | string | null | Product of the plan the row belongs to (null on a plain deposit). |
| `amount` | number | Amount in USD, always positive. Read direction for the sign. |
| `direction` | string | "out" = taken from your balance, "in" = added to it. |
| `status` | string | CONFIRMED, CANCELED, PENDING, etc. A CANCELED purchase is one that was deleted or failed; any money returned for it appears as its own "in" row. |
| `actionBy` | string | "User" (you), "Admin" or "AutoSystem" (an automatic refund or adjustment). |
| `referenceType / reference` | string | "order" + the order ID, or "extend" + the renewal ID. Empty on a plain deposit. |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/billing?page=1&pageSize=20" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"transactions": [
  {
    "id": "c_283c37e6-8efe-40df-83b8-dcb25ff4edfa",
    "kind": "extend",
    "label": "Added 10 GB",
    "proxyType": "v2resi",
    "amount": 7,
    "direction": "out",
    "status": "CONFIRMED",
    "paymentMethod": "Balance",
    "actionBy": "User",
    "createdAt": "2026-10-09T02:57:36.692Z",
    "referenceType": "order",
    "reference": "700c853f-9818-4162-bed9-10003d3cb85e"
  }
]
  },
  "pagination": {
"page": 1,
"pageSize": 20,
"total": 57,
"totalPages": 3,
"hasMore": true
  }
}
```

### Store & Products

#### `GET /store`

Get all available proxy packages with your custom reseller pricing. Custom ISP and Dedicated ISP packages are priced per IP.

Custom ISP packages (proxyType Isp) carry a customIsp object with the tier, the plan length and the add-on prices. One package is one tier and one length, and resellerPrice is your price per IP for it. Their locations[] is empty: the cities come from GET /custom-isp/catalog, which is also the easier way to list your Custom ISP prices.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number (starts at 1) |
| `pageSize` | number | no | Items per page (max 100) |
| `proxyType` | string | no | Filter by proxy type. Use the proxyType value returned for each product in /store (products[].proxyType). |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `products[].packageId` | string | Package ID for purchasing |
| `products[].title` | string | Product title/name |
| `products[].originalPrice` | number | Original retail price in USD. For ISP this is the per-IP price. |
| `products[].resellerPrice` | number | Your reseller price in USD. For ISP this is the per-IP price - multiply by quantity for total. |
| `products[].discount` | number | Discount percentage vs original price |
| `products[].proxyType` | string | Proxy type. Exact values to filter by: v2resi (Standard Residential), resix (Premium Residential), Isp (Custom ISP), IspP (Dedicated ISP), DatacenterP (Datacenter), Ipv6p (IPv6), RotatingMobile. See the Product types table in the Overview. |
| `products[].bandwidth` | string|null | Bandwidth in GB. null for ISP packages. "0" on IPv6 = unlimited bandwidth (plan is speed-capped instead - see speed). |
| `products[].ips` | number|null | Datacenter (DatacenterP) only - number of dedicated IPs in the package. Your country allocation at purchase must total this number. |
| `products[].speed` | number|null | Speed cap in Mbps (e.g. 30, 100, 1000). Set on speed-based plans - IPv6 unlimited, where it identifies the plan tier. 0 or null = no speed cap. |
| `products[].days` | number | Package duration in days |
| `products[].pricingNote` | string|null | Custom ISP and Dedicated ISP - human-readable pricing summary e.g. "$2.50/IP × 1-10 IPs" |
| `products[].minQty` | number|null | Custom ISP and Dedicated ISP - minimum quantity (IPs) per order |
| `products[].maxQty` | number|null | Custom ISP and Dedicated ISP - maximum quantity (IPs) per order. null means unlimited. |
| `products[].isSubnet` | boolean | Dedicated ISP only - a /24 subnet block. Subnet plans are NOT buyable via the API (they 400 on purchase); arrange them with support. |
| `products[].locations` | array | Always an empty array. Custom ISP cities come from GET /custom-isp/catalog. Dedicated ISP is pool-based, with no country to pick. |
| `products[].customIsp` | object|null | Custom ISP only, null on every other product. { tier, tierLabel, length, carrierFeePerIp, replacementPrice, maxReplacements }. tier is "standard" or "premium", tierLabel is "Standard" or "Clean Score". length is "1_day", "7_days", "1_month", "2_months" or "3_months". carrierFeePerIp is added per IP when the order picks a carrier, replacementPrice is the price of one IP replacement bought with the plan, maxReplacements is 5. |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/store?page=1&pageSize=20&proxyType=Isp" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"products": [
  {
    "packageId": "a1b2c3d4-0003-4a5b-8c9d-0e1f2a3b4c5d",
    "title": "Custom ISP Standard - 1 Month",
    "description": "ISP proxies, city and carrier of your choice",
    "originalPrice": 4,
    "resellerPrice": 3.2,
    "discount": 20,
    "savings": 0.8,
    "proxyType": "Isp",
    "bandwidth": null,
    "ips": null,
    "speed": null,
    "days": 30,
    "hidden": false,
    "pricingNote": "$3.2/IP × 1-100 IPs",
    "minQty": 1,
    "maxQty": 100,
    "locations": [],
    "customIsp": {
      "tier": "standard",
      "tierLabel": "Standard",
      "length": "1_month",
      "carrierFeePerIp": 0.35,
      "replacementPrice": 1.1,
      "maxReplacements": 5
    }
  }
],
"totalProducts": 1
  },
  "pagination": {
"page": 1,
"pageSize": 20,
"totalPages": 1,
"hasMore": false
  }
}
```

#### `GET /custom-isp/catalog`

Everything you need to build a Custom ISP order. Custom ISP is static ISP proxies where the buyer picks the tier, the plan length, the city and, optionally, the carrier and UDP support.

plans lists one package per tier and length that has a reseller price on your account. pricePerIp is YOUR price per IP; only the tiers and lengths you were given a price for are listed. Send the packageId to POST /order. countries lists the cities with IPs in stock right now for the tier and UDP choice you asked for, with how many IPs each has. Send a city's location string exactly as returned.

The tiers are standard and premium. premium is sold as Clean Score: the IPs are checked right before delivery, with a fraud score of 0 when delivered. Checking an IP on a score site through the proxy raises its score, so check it from a connection that is not using the proxy.

The IP counts come from a cache that is at most a minute old. POST /order checks stock again before charging. When Custom ISP is not on sale (switched off, or new reseller purchases are paused) the payload is just { "enabled": false }. When you have no plan in the tier you asked for, countries is empty. Limited to 40 requests per minute per account.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `tier` | string | no | "standard" or "premium" (Clean Score). Picks which tier's cities and IP counts are listed. plans always holds every tier you have a price for. |
| `udp` | string | no | "1" to count only IPs with UDP support. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `enabled` | boolean | false when Custom ISP is not on sale right now. Nothing else is sent then. |
| `tiers` | array | Tiers you have at least one plan in. |
| `plans[tier][length].packageId` | string | Send as packageId to POST /order. |
| `plans[tier][length].pricePerIp` | number | Your reseller price for one IP for this length. |
| `plans[tier][length].days` | number | Plan length in days. |
| `plans[tier][length].minQty / maxQty` | number | Allowed IPs per order. |
| `plans[tier][length].replacementPrice` | number|null | Price of one IP replacement bought with this plan. null when replacements are not sold on it. |
| `carrierFeePerIp` | number | Added per IP when the order picks a carrier (ispData.asn). |
| `maxReplacements` | number | Most IP replacements one order can be bought with (5). |
| `countries[].cities[].location` | string | "Country - City". Send exactly this as ispData.location and to GET /custom-isp/carriers. |
| `countries[].cities[].count` | number | IPs in stock in that city for this tier and UDP choice. |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/custom-isp/catalog?tier=standard&udp=0" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"enabled": true,
"tiers": ["standard", "premium"],
"plans": {
  "standard": {
    "1_month": { "packageId": "a1b2c3d4-0003-4a5b-8c9d-0e1f2a3b4c5d", "pricePerIp": 3.2, "days": 30, "minQty": 1, "maxQty": 100, "replacementPrice": 1.1 },
    "3_months": { "packageId": "a1b2c3d4-0005-4a5b-8c9d-0e1f2a3b4c5d", "pricePerIp": 9, "days": 90, "minQty": 1, "maxQty": 100, "replacementPrice": 1.1 }
  },
  "premium": {
    "1_month": { "packageId": "a1b2c3d4-0103-4a5b-8c9d-0e1f2a3b4c5d", "pricePerIp": 4.5, "days": 30, "minQty": 1, "maxQty": 100, "replacementPrice": 1.1 }
  }
},
"carrierFeePerIp": 0.35,
"maxReplacements": 5,
"countries": [
  {
    "country": "United States",
    "total": 1840,
    "cities": [
      { "city": "New York City", "location": "United States - New York City", "count": 412 },
      { "city": "Ashburn", "location": "United States - Ashburn", "count": 298 }
    ]
  }
]
  },
  "pagination": {}
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 503 | Live stock is unavailable right now. Please try again in a minute. | Stock could not be read just now. Try again shortly. |
| 429 | Too many requests. Please wait a moment. | More than 40 calls in a minute. Cache the result for a minute. |

#### `GET /custom-isp/carriers`

The carriers (ASNs) with IPs in stock in one city, best first, for one tier and UDP choice. Picking a carrier is optional. To pin an order to one, send its asn as ispData.asn on POST /order; every IP then costs carrierFeePerIp more.

location must be a city exactly as GET /custom-isp/catalog returns it (a country name answers an empty list: buying for any city in a country always uses any carrier). Use the same tier and udp you will buy. Cached for about two minutes. Limited to 40 requests per minute per account.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `location` | string | yes | A city from the catalog, e.g. "United States - New York City". |
| `tier` | string | no | "standard" or "premium". The tier you will buy. |
| `udp` | string | no | "1" for UDP-capable IPs only. The same choice you will buy. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `carrierFeePerIp` | number | Added per IP when the order picks one of these carriers. |
| `carriers[].asn` | number | Send as ispData.asn on POST /order. |
| `carriers[].name` | string | Carrier name. |
| `carriers[].stock` | number | IPs on that carrier in stock in this city. |

Request:

```bash
curl -G "https://catproxies.com/api/v1/public/custom-isp/carriers" \
  --data-urlencode "location=United States - New York City" \
  --data-urlencode "tier=standard" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"carrierFeePerIp": 0.35,
"carriers": [
  { "asn": 7922, "name": "Comcast Cable Communications", "stock": 96 },
  { "asn": 701, "name": "Verizon Business", "stock": 41 }
]
  },
  "pagination": {}
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | location is required (a city from GET /custom-isp/catalog). | location is missing. |
| 400 | This city is not available. | location is not a city from the catalog, or it has no stock for this tier and UDP choice. |
| 503 | Carrier stock is unavailable right now. Please try again in a minute. | Stock could not be read just now. Try again shortly. |
| 429 | Too many requests. Please wait a moment. | More than 40 calls in a minute. |

#### `POST /custom-isp/domain`

Sets up your own hostname for Custom ISP lines, so your customers get your name, with the same Auto option we use. Send a subdomain of a domain you own (e.g. isp.yourbrand.com, one per account). The reply lists 2 NS records: add them at your DNS provider for that name (and no A or CNAME records). Until they are seen, status is "pending" and your lines keep using plain server IPs; GET /custom-isp/domain switches it to "active" once the records are in place. Then {domain} is Auto (the closest server to whoever connects) and <location>.{domain} is one location, and dead servers are taken out automatically.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | yes | A subdomain of a domain you own, at least 3 parts, e.g. isp.yourbrand.com. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `domain` | string|null | Your hostname, or null when none is set. |
| `status` | string | "none", "pending" (waiting for your NS records; lines use plain server IPs) or "active" (lines use your hostname). |
| `records` | array | The 2 NS records to add at your DNS provider: { type, name, value }. |
| `hosts` | object | auto plus one name per location (us-east, us-central, us-west, ca, uk, nl, de, fi, sg, jp, au). Pass the id as ?region= on GET /order/:id/proxies. |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/custom-isp/domain" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "isp.yourbrand.com" }'
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
    "domain": "isp.yourbrand.com",
    "status": "pending",
    "nameservers": ["kiki.bunny.net", "coco.bunny.net"],
    "records": [
      { "type": "NS", "name": "isp.yourbrand.com", "value": "kiki.bunny.net" },
      { "type": "NS", "name": "isp.yourbrand.com", "value": "coco.bunny.net" }
    ],
    "hosts": {
      "auto": "isp.yourbrand.com",
      "us-east": "us-east.isp.yourbrand.com",
      "de": "de.isp.yourbrand.com"
    },
    "activeAt": null
  }
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | Please use a subdomain of a domain you own, e.g. isp.yourbrand.com. | Not a valid subdomain. |
| 409 | You already have a hostname set. Remove it first to use a different one. | One hostname per account. DELETE it first. |
| 409 | This hostname is already in use. | Another account uses it. |

#### `GET /custom-isp/domain`

Your hostname, its status and the names your lines use. While the status is "pending" every call also checks your NS records (at most every 20 seconds) and switches it to "active" when they are in place. DNS changes can take up to an hour.

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `domain` | string|null | Your hostname, or null when none is set. |
| `status` | string | "none", "pending" (waiting for your NS records; lines use plain server IPs) or "active" (lines use your hostname). |
| `records` | array | The 2 NS records to add at your DNS provider: { type, name, value }. |
| `hosts` | object | auto plus one name per location (us-east, us-central, us-west, ca, uk, nl, de, fi, sg, jp, au). Pass the id as ?region= on GET /order/:id/proxies. |

Request:

```bash
curl "https://catproxies.com/api/v1/public/custom-isp/domain" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
    "domain": "isp.yourbrand.com",
    "status": "active",
    "nameservers": ["kiki.bunny.net", "coco.bunny.net"],
    "records": [
      { "type": "NS", "name": "isp.yourbrand.com", "value": "kiki.bunny.net" },
      { "type": "NS", "name": "isp.yourbrand.com", "value": "coco.bunny.net" }
    ],
    "hosts": {
      "auto": "isp.yourbrand.com",
      "us-east": "us-east.isp.yourbrand.com",
      "de": "de.isp.yourbrand.com"
    },
    "activeAt": "2026-10-14T09:12:00.000Z"
  }
}
```

#### `DELETE /custom-isp/domain`

Removes your hostname. Your Custom ISP lines go back to plain server IPs right away. Remove the NS records on your side too.

Request:

```bash
curl -X DELETE "https://catproxies.com/api/v1/public/custom-isp/domain" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": { "removed": true }
}
```

#### `GET /datacenterp-countries`

Get available countries and IP stock for DatacenterP proxies. Use this to check which countries are available and how many IPs can be allocated before purchasing.

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `countries` | array | Array of available countries with stock |
| `countries[].code` | string | ISO country code (use in datacenterPData.country_proxies) |
| `countries[].name` | string | Country name |
| `countries[].available` | number | Number of IPs currently available |
| `allowedCountryCodes` | array | List of valid country codes for DatacenterP |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/datacenterp-countries" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"countries": [
  { "code": "US", "name": "United States", "available": 4832 },
  { "code": "CA", "name": "Canada", "available": 1205 },
  { "code": "DE", "name": "Germany", "available": 3410 },
  { "code": "GB", "name": "United Kingdom", "available": 2108 },
  { "code": "NL", "name": "Netherlands", "available": 1567 }
],
"allowedCountryCodes": ["US", "CA", "DE", "GB", "NL"]
  }
}
```

### Orders

#### `GET /orders`

Get all your reseller orders with pagination, search, and sorting capabilities.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | number | no | Page number |
| `pageSize` | number | no | Items per page (max 100) |
| `search` | string | no | Search by order ID |
| `orderBy` | string | no | Field to sort by |
| `order` | string | no | Sort order (asc/desc) |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `orders[].id` | string | Order ID |
| `orders[].package_title` | string | Package title |
| `orders[].status` | string | Order status (CONFIRMED, PENDING) |
| `orders[].amount` | number | Amount paid (excluding tax) |
| `orders[].createdAt` | string | When order was created (ISO 8601) |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/orders?page=1&pageSize=10" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"orders": [
  {
    "id": "700c853f-9818-4162-bed9-10003d3cb85e",
    "package_title": "Standard Residential 2GB",
    "package_bandwidth": 2,
    "package_days": 90,
    "package_proxyType": "v2resi",
    "status": "CONFIRMED",
    "amount": 2.23,
    "createdAt": "2025-11-13T12:05:09.820Z"
  }
]
  },
  "pagination": {
"page": 1,
"pageSize": 10,
"totalOrders": 42,
"totalPages": 5,
"hasMore": true
  }
}
```

#### `POST /order`

Create a new proxy purchase order using your reseller balance. Returns full order details including proxy credentials.

For Dedicated ISP (IspP) proxies, pass ispData with just the quantity - these are pool-based, so there is NO country (locations is empty). Total = resellerPrice × quantity, within the package minQty to maxQty range. The term is a fixed 30 days. The individual IP:port lines are fetched from GET /order/:id/proxies (not on GET /order/:id).

For Custom ISP proxies (proxyType Isp, packageId from GET /custom-isp/catalog), pass ispData with { quantity, location, asn?, udp?, replacements? }. quantity and location are required. You are charged from your balance: your price per IP x quantity, plus carrierFeePerIp x quantity when you set asn, plus replacements x replacementPrice. No VAT. Live stock is checked before you are charged, and a city or carrier that ran out is refused with nothing charged. If no IP can be delivered on the spot, you get HTTP 202 with payload.settingUp true and payload.order (status PENDING): you have been charged and the order retries by itself after 2, 7 and 12 minutes. Do not order again; read GET /order/:id until it shows the lines, or until it is CANCELED, which means no IP could be delivered and the price is already back on your balance. Ordering the exact same choice again (same plan, location, asn, udp, extra checks and quantity) while an earlier one is still retrying cancels and refunds the earlier one, and only the newest keeps retrying. If your last 3 orders for one exact choice all ended with no IP, that choice is refused for a while with HTTP 409 and payload.code CUSTOM_ISP_CHOICE_BLOCKED (nothing charged, payload.retryAfterMinutes says when it opens again): pick another city or carrier. The lines come from GET /order/:id/proxies.

For Datacenter (DatacenterP) proxies, you must include datacenterPData with country allocation and addon configuration.

For IPv6 (Ipv6p) proxies, no extra fields are needed - just the packageId.

Optional customer tagging: you can add customerId (a string: your own id or label for the end customer) to group that customer's plans together in your panel. It is completely optional and changes nothing about the purchase; leave it out and orders behave exactly as before. You can also tag or re-tag an existing order later with POST /order/:id/customer.

IMPORTANT (Dedicated ISP and Custom ISP): on the rare occasion an order is charged but needs manual review, you get an error whose message says the order "needs manual review... will be delivered within 24 hours or refunded". READ THE MESSAGE, NOT ONLY THE STATUS: this case currently comes back as HTTP 500, so a blanket "retry on 5xx" wrapper will buy the plan a SECOND time and charge you again. The charge already stands and a human is finishing the order. Never auto-retry POST /order at all: it is not idempotent and there is no idempotency key, so every retry is a new order and a new charge. If a request times out, look the order up with GET /orders (tag it with customerId when you buy and you can match it) instead of sending it again. Renewals (POST /order/:id/extend) are not idempotent either: a second call after the first one succeeded renews, and charges, a second time. Only a call that arrives while a Datacenter, IPv6 or Custom ISP renewal of the same order is still running is refused (409, not charged). If a renewal times out, read GET /order/:id and compare expired_at before sending it again.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `packageId` | string | yes | Package ID from /store endpoint |
| `customerId` | string | no | OPTIONAL. Your own id or label for the end customer this plan is for. Passing it groups that customer's plans together in your panel. Purely for your own organisation, it changes nothing about the order, the price or the proxy. Omit it and everything works exactly as before. |
| `ispData` | object | no | Custom ISP and Dedicated ISP only. The fields below. |
| `ispData.quantity` | integer | no | Number of IPs to purchase. Must be within the package minQty to maxQty range. Default: 1 (required on Custom ISP). Charged: resellerPrice × quantity (on Custom ISP plus the carrier fee and replacements). |
| `ispData.location` | string | no | Custom ISP only, and required there. The city exactly as GET /custom-isp/catalog returns it, e.g. "United States - New York City". Or just the country (countries[].country, e.g. "United States") for IPs from any city in it; its stock is countries[].total. A country can't be combined with ispData.asn. |
| `ispData.asn` | number | no | Custom ISP only. A carrier from GET /custom-isp/carriers for that city. Adds carrierFeePerIp per IP. Omit for any carrier. |
| `ispData.udp` | boolean | no | Custom ISP only. true = only IPs with UDP support. Default false. |
| `ispData.ip2locationIsp` | boolean | no | Custom ISP only, optional, beta. true = you only get IPs that IP2Location (a well-known IP lookup database) marks as "ISP", meaning a home or fixed internet line, not business or hosting. Checked on every IP right before delivery, at no extra cost. Not every IP has this mark, so fewer IPs are available than the stock shows: for bigger orders use a big city or a whole country. On Clean Score it comes on top of the usual fraud score 0 check on IPQS. Default false. |
| `ispData.replacements` | integer | no | Custom ISP only. IP replacements bought with the plan, 0 to 5, each at the plan's replacementPrice. Used with POST /order/:id/replace. Not renewed. Default 0. IP replacements can only be bought together with the order: they cannot be added later, so choose how many you need when you buy. |
| `datacenterPData` | object | no | Required for DatacenterP proxies. Contains country allocation and addon configuration. |
| `datacenterPData.country_proxies` | object | no | Country code → IP count mapping. Allowed: US, CA, DE, GB, NL. Total must equal package IP count. A single worldwide-rotating line is only available when ALL five countries are included; otherwise rotate per country via rotating_gateways. |
| `datacenterPData.high_concurrency` | boolean | no | Enable high concurrency addon (+25% price) |
| `datacenterPData.high_priority` | boolean | no | Enable high priority addon (+25% price) |
| `datacenterPData.whitelisted_ips` | boolean | no | Enable extra whitelisted IPs addon (+25% price) |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `order.id` | string | Created order ID |
| `order.status` | string | Order status (CONFIRMED) |
| `proxyCredentials.username` | string | Proxy authentication username |
| `proxyCredentials.password` | string | Proxy authentication password |
| `proxyCredentials.hostip` | string | Proxy hostname or IP |
| `proxyCredentials.port` | string | The DEFAULT port, as a string. On Standard Residential it is a RANGE, e.g. "9000-12000", not a single port: do not concatenate it blindly into host:port. Read the exact ports from proxyCredentials.endpoints / ports, or from the per-product guide below (Standard Residential 9000 rotating, 10000 sticky, 11000/12000 SOCKS5). |

Request:

```bash
# Standard purchase
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "6f19b4e7-1b1e-4ed5-85e4-41e6bbe07101" }'

# Standard purchase - optionally tagged to one of your customers
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "6f19b4e7-1b1e-4ed5-85e4-41e6bbe07101", "customerId": "acme-co" }'

# Custom ISP - 5 IPs in New York City on one carrier, with 2 IP replacements
# (packageId and location from GET /custom-isp/catalog, asn from /custom-isp/carriers)
# Charged: 3.20 x 5 + 0.35 x 5 + 1.10 x 2 = 19.95
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
"packageId": "a1b2c3d4-0003-4a5b-8c9d-0e1f2a3b4c5d",
"ispData": { "quantity": 5, "location": "United States - New York City", "asn": 7922, "udp": false, "replacements": 2 }
  }'

# DatacenterP (country allocation must total the package IP count):
curl -X POST "https://catproxies.com/api/v1/public/order" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
"packageId": "pkg-datacenterp-100",
"datacenterPData": {
  "country_proxies": { "US": 60, "DE": 20, "GB": 20 },
  "high_concurrency": true,
  "high_priority": false,
  "whitelisted_ips": false
}
  }'
```

Response:

```json
{
  "type": "response",
  "message": "Purchase successful! Your proxy has been activated.",
  "payload": {
"order": {
  "id": "8a08caa7-e1f2-487e-aedb-9ebc018ab8f0",
  "package_title": "Residential 1GB",
  "status": "CONFIRMED",
  "amount": 2.23,
  "proxyCredentials": {
    "username": "hftjvbtwgxxxceb205604",
    "password": "ngnstzylne",
    "hostip": "203.0.113.10",
    "port": "9000-12000",
    "available_bandwidth": 1,
    "bandwidth_left": 1,
    "expired_at": "2026-02-27T20:46:02.383Z"
  }
}
  }
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | We're temporarily out of stock for this plan. Your payment went through and your order has been flagged to our team. It will be delivered manually within 24 hours, or you'll be refunded. No action is needed from you. | Dedicated ISP. The payment is kept and we retry automatically for up to 3 hours, then refund your balance if it still cannot be delivered. Do not order again. |
| 500 | Your payment went through, but this order needs manual attention from our team. It has been flagged automatically and will be delivered within 24 hours, or you'll receive a refund. Please don't re-order. Contact us if you'd like an update. | Dedicated ISP, unclear result. You were charged and our team delivers or refunds the order. Do not order again. |
| 400 | Insufficient balance | Top up and try again. Nothing was charged. |
| 400 | ispData with location and quantity is required for Custom ISP | Custom ISP: send ispData.location (a city from GET /custom-isp/catalog) and ispData.quantity. |
| 400 | Quantity must be between 1 and 100. | The quantity is outside the plan's minQty and maxQty. Nothing was charged. |
| 400 | This city is out of stock right now. Please pick another one. | Custom ISP: the city ran out, or location is not a city from the catalog. Nothing was charged. |
| 400 | This carrier is out of stock in this city right now. Please pick another one. | Custom ISP: no IPs left on that asn in that city. Nothing was charged. |
| 400 | Only 3 IPs are available for this choice right now. Please lower the quantity or change your filters. | Custom ISP: fewer IPs in stock than you asked for. Nothing was charged. |
| 400 | You can add up to 5 IP replacements. | Custom ISP: replacements is above 5. Nothing was charged. |
| 202 | Your payment went through and your plan is being set up. This usually takes a few minutes. It will appear in Active Plans on its own, so there is no need to order again. | Custom ISP: charged, no IP yet. It retries by itself after 2, 7 and 12 minutes; payload.order is the order to follow with GET /order/:id. If it still gets no IP it becomes CANCELED and the price is refunded to your balance automatically. Do not order again. |
| 409 | Your last 3 orders for this exact choice could not get an IP, so we are not taking it again for now. Nothing was charged. Please pick another city or carrier, or try this one again later. | Custom ISP: payload.code is CUSTOM_ISP_CHOICE_BLOCKED. Nothing was charged. Pick another city or carrier, or wait payload.retryAfterMinutes. |
| 403 | This product is not available at the moment. Please try again later. | New reseller purchases of this product or plan are paused, or the plan is not in your store. Nothing was charged. |
| 503 | Live stock is unavailable right now. Please try again in a minute. | Custom ISP: we could not check stock just now. Nothing was charged; try again shortly. |
| 500 | We could not activate this proxy right now, so nothing was charged - your balance has been refunded. Please try again in a few minutes. | Delivery failed and your balance is back. Safe to buy again later. |
| 500 | Your payment went through, but this order needs manual review by our team. It will be delivered within 24 hours or refunded. Please don't re-order. | You WERE charged and our team finishes the order. Do NOT retry: a retry is a new order and a new charge. |

#### `GET /order/:id`

Get detailed information about a specific order including proxy connection details, usage figures, and whitelisted IPs. This is also where you read the new end date (proxyCredentials.expired_at) after a renewal.

The fields under proxyCredentials depend on the product. Datacenter and IPv6 are shown below.

Datacenter: every IP in ip_list is a dedicated proxy on port_http (1338) and port_socks5 (1339). For ROTATING (a new IP on every new connection), connect to rotating_gateways.<COUNTRY> and append -ttl-0 to the username. The rotating gateways are picked per order, so read them from here each time instead of saving them. rotating_gateways and rotating_gateway_sets only list the countries this order has IPs in (a gateway for any other country would not accept the order). rotating_gateway_sets lists every usable gateway per country. The US has two gateways and each one reaches about half of the order's US IPs, so to give your customer your own hostname for US rotating, point its A-records at EVERY IP in rotating_gateway_sets.US (normally two). rotating_bound_ip_conflicts lists the countries where the order owns the only gateway: rotating there keeps giving the same IP while an IP is whitelisted.

IPv6: the exit country is picked by the gateway you connect to (endpoints: ww, us, de, gb, nl), on ports.http (1338) or ports.socks5 (1339). usage is null on unlimited (speed-capped) plans.

Custom ISP: proxyCredentials has the order's one username and password, port "1337" (HTTP and SOCKS5 on the same port), auth_mode "logpas" and a note. The per-IP lines come from GET /order/:id/proxies, and POST /order/:id/replace swaps an IP. package_region holds what was bought, as a JSON string: { kind: "customisp", tier, location, asn, udp, length, replacements, carrierName }.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The order ID |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `order.id` | string | Order ID |
| `order.status` | string | Order status |
| `proxyCredentials.username` | string | Proxy username |
| `proxyCredentials.password` | string | Proxy password |
| `proxyCredentials.total_bandwidth` | number|null | Total bandwidth on the plan in GB (used_bandwidth + bandwidth_left). null on unlimited / speed-capped plans that have no GB cap. |
| `proxyCredentials.used_bandwidth` | number | Bandwidth used so far, in GB. |
| `proxyCredentials.bandwidth_left` | number | Remaining bandwidth in GB. |
| `proxyCredentials.available_bandwidth` | number | Remaining bandwidth in GB. Legacy alias of bandwidth_left, kept for backward compatibility. Prefer bandwidth_left and total_bandwidth. |
| `proxyCredentials.expired_at` | string | End date of the plan (ISO 8601). Read it again after a renewal to see the new end date. |
| `proxyCredentials.note` | string | Datacenter, IPv6 and some other products: plain-text connection help for the product. Informational only, do not parse it. |
| `proxyCredentials.ip_list` | array | Datacenter only. The order's IPs grouped by country, then city: [{ iso2, name, count_ips, cities: [{ name, ips: [] }] }]. Flatten cities[].ips for every address. Refreshed live on each call. |
| `proxyCredentials.country_list` | object|null | Datacenter only. { countries: { "US": 60, ... }, total }: how many IPs the order has per country, and in total. |
| `proxyCredentials.port_http` | string | Datacenter only. "1338", the HTTP/HTTPS port on every IP and rotating gateway. |
| `proxyCredentials.port_socks5` | string | Datacenter only. "1339", the SOCKS5 port on every IP and rotating gateway. |
| `proxyCredentials.whitelist_ip` | array | Datacenter: the IPs whitelisted for credential-less access (see /order/:id/whitelist). |
| `proxyCredentials.rotating_gateways` | object | Datacenter only. One rotating gateway IP per country: { "US": "<ip>", "CA": "<ip>", ... }. Picked for this order, so read it from the API instead of saving it. Only the countries this order has IPs in. |
| `proxyCredentials.rotating_gateway_sets` | object | Datacenter only. Every usable rotating gateway per country: { "US": ["<ip>", "<ip>"], ... }. The US has two, each reaching about half of the order's US IPs: point your own hostname's A-records at all of them. |
| `proxyCredentials.rotating_bound_ip_conflicts` | array | Datacenter only. Country codes where the order owns the only rotating gateway. Rotating there keeps giving the same IP while an IP is whitelisted. Usually empty. |
| `proxyCredentials.hostip` | string | IPv6: the default (worldwide) gateway IP. Other products: their host. |
| `proxyCredentials.port` | string | IPv6: "1338" (HTTP). SOCKS5 is in ports.socks5. |
| `proxyCredentials.endpoints` | object | IPv6: one gateway IP per exit country: { ww, us, de, gb, nl }. The same login works on all of them. Premium Residential (resix): { http: { americas, europe, asia }, socks5: { americas, europe, asia } }, each an "ip:port". |
| `proxyCredentials.isp_pools` | array | Premium Residential (resix) only. The six dedicated ISP pools this order can also use with the same login: [{ id, name, country, protocol, endpoint }], e.g. { id: "us", name: "United States", country: "US", protocol: "http", endpoint: "<ip>:6000" }. One country per pool, HTTP only, no targeting flags (bare password or -session-<8>_lifetime-<h> only). Read the endpoint from here each time rather than saving it. |
| `proxyCredentials.ports` | object | IPv6: { http: "1338", socks5: "1339" }. |
| `proxyCredentials.usage` | object|null | IPv6: { available_bandwidth, used_bandwidth, bandwidth_left } in GB on bandwidth plans; null on unlimited (speed-capped) plans. |
| `proxyCredentials.auth_mode` | string | ISP: always "logpas" (username and password). |
| `package_region` | string|null | Custom ISP: what was bought, as a JSON string { kind: "customisp", tier, location, asn, udp, length, replacements, carrierName }. asn and carrierName are null when no carrier was picked. |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/order/700c853f-9818-4162-bed9-10003d3cb85e" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// Standard Residential order
{
  "type": "response",
  "message": "success",
  "payload": {
"order": {
  "id": "700c853f-9818-4162-bed9-10003d3cb85e",
  "package_title": "Residential 1GB",
  "status": "CONFIRMED",
  "proxyCredentials": {
    "username": "mtpeqbcevnemewc201120",
    "password": "jonxybfeuu",
    "hostip": "203.0.113.10",
    "port": "9000-12000",
    "total_bandwidth": 2,
    "used_bandwidth": 0,
    "bandwidth_left": 2,
    "available_bandwidth": 2,
    "expired_at": "2026-02-20T11:48:45.000Z"
  }
}
  },
  "pagination": {}
}

// Datacenter order (all IPs are documentation examples; read yours from the API)
{
  "type": "response",
  "message": "success",
  "payload": {
"order": {
  "id": "c5c7c6b5-e488-420c-b436-e90e86031751",
  "package_title": "Datacenter Proxies - 1 Month - 100 IPs - 250GB",
  "package_proxyType": "DatacenterP",
  "package_ips": 100,
  "package_bandwidth": 250,
  "package_days": 30,
  "status": "CONFIRMED",
  "extendOrders": [],
  "proxyCredentials": {
    "username": "dcuser482913",
    "password": "dcpass91827",
    "total_bandwidth": 250,
    "used_bandwidth": 12.4,
    "bandwidth_left": 237.6,
    "available_bandwidth": 237.6,
    "days": 30,
    "expired_at": "2026-10-11T10:00:00.000Z",
    "port_http": "1338",
    "port_socks5": "1339",
    "whitelist_ip": [],
    "ip_list": [
      {
        "iso2": "US",
        "name": "United States",
        "count_ips": 60,
        "cities": [{ "name": "New York", "ips": ["203.0.113.24", "203.0.113.57", "..."] }]
      },
      {
        "iso2": "DE",
        "name": "Germany",
        "count_ips": 40,
        "cities": [{ "name": "Frankfurt", "ips": ["203.0.113.140", "..."] }]
      }
    ],
    "country_list": { "countries": { "US": 60, "DE": 40 }, "total": 100 },
    "rotating_gateways": {
      "US": "198.51.100.10",
      "DE": "198.51.100.40"
    },
    "rotating_gateway_sets": {
      "US": ["198.51.100.10", "198.51.100.20"],
      "DE": ["198.51.100.40"]
    },
    "rotating_bound_ip_conflicts": [],
    "note": "Datacenter. Every IP in ip_list is a dedicated proxy: ... (connection help as plain text)"
  }
}
  },
  "pagination": {}
}

// Premium Residential order (resix). isp_pools are the dedicated ISP pools, HTTP only
{
  "type": "response",
  "message": "success",
  "payload": {
"order": {
  "id": "3c4d5e6f-7a8b-4c9d-8e0f-a1b2c3d4e5f6",
  "package_title": "Premium Residential 5GB",
  "package_proxyType": "resix",
  "status": "CONFIRMED",
  "proxyCredentials": {
    "username": "hftjvbtwgxxxceb205604",
    "password": "ngnstzylne",
    "total_bandwidth": 5,
    "used_bandwidth": 1.2,
    "bandwidth_left": 3.8,
    "available_bandwidth": 3.8,
    "expired_at": "2026-12-01T10:00:00.000Z",
    "hostip": "203.0.113.20",
    "port": "6011",
    "ip": null,
    "usage": { "available_bandwidth": 3.8, "used_bandwidth": 1.2, "bandwidth_left": 3.8 },
    "endpoints": {
      "http": { "americas": "203.0.113.20:6011", "europe": "203.0.113.21:6012", "asia": "203.0.113.22:6013" },
      "socks5": { "americas": "203.0.113.20:6014", "europe": "203.0.113.21:6015", "asia": "203.0.113.22:6016" }
    },
    "isp_pools": [
      { "id": "us", "name": "United States", "country": "US", "protocol": "http", "endpoint": "203.0.113.30:6000" },
      { "id": "us_tickets", "name": "United States (Tickets)", "country": "US", "protocol": "http", "endpoint": "203.0.113.30:6001" },
      { "id": "uk", "name": "United Kingdom", "country": "GB", "protocol": "http", "endpoint": "203.0.113.31:6003" },
      { "id": "de", "name": "Germany", "country": "DE", "protocol": "http", "endpoint": "203.0.113.31:6006" },
      { "id": "de_dtag", "name": "Germany (DTAG)", "country": "DE", "protocol": "http", "endpoint": "203.0.113.31:6007" },
      { "id": "au", "name": "Australia", "country": "AU", "protocol": "http", "endpoint": "203.0.113.32:6002" }
    ],
    "note": "Premium Residential X. Connect through the endpoint that matches your target country's region ... (connection help as plain text)"
  }
}
  },
  "pagination": {}
}

// IPv6 bandwidth order (for an unlimited plan, usage and total_bandwidth are null)
{
  "type": "response",
  "message": "success",
  "payload": {
"order": {
  "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
  "package_title": "IPv6 50GB",
  "package_proxyType": "Ipv6p",
  "status": "CONFIRMED",
  "proxyCredentials": {
    "username": "ipv6user291",
    "password": "v6pass817",
    "total_bandwidth": 50,
    "used_bandwidth": 12.6,
    "bandwidth_left": 37.4,
    "available_bandwidth": 37.4,
    "expired_at": "2026-10-02T08:30:00.000Z",
    "hostip": "203.0.113.60",
    "port": "1338",
    "ip": null,
    "usage": { "available_bandwidth": 37.4, "used_bandwidth": 12.6, "bandwidth_left": 37.4 },
    "endpoints": {
      "ww": "203.0.113.60",
      "us": "203.0.113.60",
      "de": "203.0.113.61",
      "gb": "203.0.113.62",
      "nl": "203.0.113.63"
    },
    "ports": { "http": "1338", "socks5": "1339" },
    "note": "IPv6. Pick the exit country by gateway host (see endpoints); ... (connection help as plain text)"
  }
}
  },
  "pagination": {}
}

// Custom ISP order. The per-IP lines are on GET /order/:id/proxies
{
  "type": "response",
  "message": "success",
  "payload": {
"order": {
  "id": "9d8e7f6a-5b4c-4d3e-8f2a-1b0c9d8e7f6a",
  "package_title": "Custom ISP - Standard - 1 Month",
  "package_proxyType": "Isp",
  "package_ips": 5,
  "package_region": "{\"kind\":\"customisp\",\"tier\":\"standard\",\"location\":\"United States - New York City\",\"asn\":7922,\"udp\":false,\"length\":\"1_month\",\"replacements\":2,\"carrierName\":\"Comcast Cable Communications\"}",
  "status": "CONFIRMED",
  "proxyCredentials": {
    "username": "cpx8f31a2",
    "password": "Rk4pX9wq",
    "expired_at": "2026-11-12T12:00:00.000Z",
    "hostip": null,
    "port": "1337",
    "ip": null,
    "auth_mode": "logpas",
    "whitelist_ip": [],
    "note": "Custom ISP. Authenticated by username:password; HTTP and SOCKS5 on the same port. Get one line per IP from GET /order/:id/proxies ..."
  }
}
  },
  "pagination": {}
}
```

#### `GET /order/:id/extendOptions`

Get available extension packages for a specific order, with the exact price POST /order/:id/extend will take from your balance. Read-only, charges nothing. It does not check whether the plan has expired: POST /order/:id/extend refuses an expired plan with 403.

• Standard Residential / Premium Residential / Rotating Mobile: returns your assigned bandwidth packs. These options have no extendType field: they always add bandwidth.
• Custom ISP (Isp): returns one option per length the plan can renew for (for example 1 day, 7 days, 1 month, 3 months), each at your resellerPrice for that length × the order's IP count, plus carrierFeePerIp × the IP count when the order was bought on a carrier. The plan's own length comes first and has current: true; length says which length an option is ("1_day", "7_days", "1_month", "2_months" or "3_months"). Only lengths you have a price for are listed. IP replacements are not renewed. extendType is "days". Omit packageId on /extend to renew for the plan's own length, or send an option's id to renew for that length.
• Dedicated ISP (IspP): returns a single option, the order's original package at resellerPrice × the order's IP count. extendType is "days" (a fixed 30-day renewal). packageId is auto-resolved from the order, so you can omit it on /extend.
• Datacenter (DatacenterP): returns a single option, the order's own package at your resellerPrice × the add-on multiplier (+25% for each add-on the plan was bought with: high_concurrency, high_priority, extra whitelist slots). extendType is "days". packageId is resolved from the order, so you can omit it on /extend. note carries the same warning as the renew window on the website: "Renewing resets your data to 250 GB for the new term. Any data you haven't used yet doesn't carry over. Your remaining days do: the new term is added on top of your current end date." (250 is the plan's own GB; note is null on a plan with unlimited bandwidth.)
• IPv6 (Ipv6p): returns a single option, the order's own package at your resellerPrice. Bandwidth plans get its GB added (extendType "bandwidth"), unlimited plans get its days (extendType "days"). packageId is resolved from the order, so you can omit it on /extend. note says which of the two applies.
• Anything not listed above: returns an empty list (extending not supported).

For Datacenter and IPv6, price is exactly what the renewal charges. No VAT is added on reseller balance renewals. An empty list on a Datacenter or IPv6 order means the order's package is missing or no longer assigned to your account; contact us. On an IPv6 bandwidth plan it can also mean the renewal would take the plan past 5000 GB in total, the most an IPv6 plan can hold.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The order ID to get extension options for |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `packages[].id` | string | Package ID. Pass as packageId in POST /extend (optional for ISP, Datacenter and IPv6). |
| `packages[].price` | number | What POST /extend charges for this option. ISP: resellerPrice × order quantity (Custom ISP: plus carrierFeePerIp × quantity when the order has a carrier). Datacenter: resellerPrice × add-on multiplier. IPv6: resellerPrice. No VAT is added. |
| `packages[].resellerPrice` | number | Your price for the package itself. For ISP this is the per-IP price. |
| `packages[].originalPrice` | number | The retail price for the same renewal (multiplied the same way as price), for comparison. |
| `packages[].discount` | number | Your discount versus originalPrice, in percent. |
| `packages[].savings` | number | originalPrice minus price, in USD. |
| `packages[].bandwidth` | string|number|null | GB in the package. Residential / Mobile: a string (e.g. "1"). Datacenter and IPv6: a number (0 or null on an unlimited plan). ISP: null. |
| `packages[].days` | number | Days in the package. Added to the end date on ISP, Datacenter and unlimited IPv6 renewals. |
| `packages[].ips` | number|null | ISP: quantity (IPs) from the order. Datacenter: the plan's IP count. Otherwise null. |
| `packages[].speed` | number|null | IPv6: speed cap in Mbps (0 or null = no cap). Otherwise null. |
| `packages[].extendType` | string | Only on ISP, Datacenter and IPv6 options. "bandwidth" when the renewal adds GB (IPv6 bandwidth plans), "days" when it adds time (ISP, Datacenter, IPv6 unlimited plans). Residential and Mobile packs have no extendType: they always add bandwidth. |
| `packages[].note` | string|null | Datacenter and IPv6 only. What the renewal does to the plan's data and days, in plain words. Worth passing on to your customer. On Datacenter: the data-reset warning, or null on unlimited bandwidth. On IPv6: what gets added (days on unlimited plans; GB plus the plan's own term on bandwidth plans). |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/order/700c853f-9818-4162-bed9-10003d3cb85e/extendOptions" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// Residential / Mobile order
{
  "payload": {
"packages": [
  {
    "id": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f",
    "title": "Residential 1GB",
    "price": 2.50,
    "resellerPrice": 2.50,
    "bandwidth": "1",
    "days": 90,
    "proxyType": "v2resi"
  }
],
"totalPackages": 5
  }
}

// Custom ISP order (5 IPs, bought on a carrier) - always 1 option
// price = 3.20 x 5 + 0.35 x 5 = 17.75
{
  "payload": {
"packages": [
  {
    "id": "a1b2c3d4-0003-4a5b-8c9d-0e1f2a3b4c5d",
    "title": "Custom ISP - Standard - 1 Month",
    "price": 17.75,
    "originalPrice": 21.75,
    "resellerPrice": 3.2,
    "discount": 18.39,
    "savings": 4,
    "bandwidth": null,
    "days": 30,
    "ips": 5,
    "proxyType": "Isp",
    "extendType": "days"
  }
],
"totalPackages": 1
  }
}

// Dedicated ISP order - always 1 option, price = resellerPrice × package_ips
{
  "payload": {
"packages": [
  {
    "id": "a3f2c1d4-9e8b-4f7a-bc3d-2e1f0a9b8c7d",
    "title": "Dedicated ISP - 30 days",
    "price": 12.50,
    "resellerPrice": 2.50,
    "bandwidth": null,
    "days": 30,
    "ips": 5,
    "proxyType": "IspP",
    "extendType": "days"
  }
],
"totalPackages": 1
  }
}

// Datacenter order - always 1 option: the order's own package.
// price = resellerPrice x add-on multiplier (here 1 add-on, so x1.25)
{
  "type": "response",
  "message": "success",
  "payload": {
"packages": [
  {
    "id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
    "title": "Datacenter Proxies - 1 Month - 100 IPs - 250GB",
    "description": "100 dedicated datacenter IPs, 250 GB, 30 days",
    "price": 100,
    "originalPrice": 125,
    "resellerPrice": 80,
    "discount": 20,
    "savings": 25,
    "bandwidth": 250,
    "days": 30,
    "proxyType": "DatacenterP",
    "ips": 100,
    "region": null,
    "speed": null,
    "extendType": "days",
    "note": "Renewing resets your data to 250 GB for the new term. Any data you haven't used yet doesn't carry over. Your remaining days do: the new term is added on top of your current end date."
  }
],
"totalPackages": 1
  },
  "pagination": {}
}

// IPv6 bandwidth order - always 1 option: the order's own package
{
  "type": "response",
  "message": "success",
  "payload": {
"packages": [
  {
    "id": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
    "title": "IPv6 50GB",
    "description": "IPv6 rotating proxies, 50 GB",
    "price": 12,
    "originalPrice": 15,
    "resellerPrice": 12,
    "discount": 20,
    "savings": 3,
    "bandwidth": 50,
    "days": 30,
    "proxyType": "Ipv6p",
    "ips": null,
    "region": null,
    "speed": 0,
    "extendType": "bandwidth",
    "note": "Renewing adds the package's GB on top of the data left, and moves the end date forward by 30 days (the plan's own term)."
  }
],
"totalPackages": 1
  },
  "pagination": {}
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 404 | Order not found | No reseller order with that id on your account. |
| 500 | Internal server error | Our side failed. Nothing was charged; safe to retry. |

#### `POST /order/:id/extend`

Renew an existing order, paid from your reseller balance. What a renewal adds depends on the product:

• Standard Residential, Premium Residential, Rotating Mobile: the GB of the package you pick from /extendOptions are added. packageId is required.
• Custom ISP: by default the same package again (same length, same IPs) at resellerPrice × the order's IP count, plus carrierFeePerIp × the IP count when the order was bought on a carrier. To renew for another length, for example a 1-day plan for 1 month, send the packageId of that length from /extendOptions; it is charged at your resellerPrice for that package. IP replacements are not renewed.
• Dedicated ISP: another full period at resellerPrice × the order's IP count.
• Datacenter: another term of the order's OWN package, at your resellerPrice × the add-on multiplier (+25% per add-on bought with the plan). Renewing resets the data to the plan's full GB for the new term. Any data not used yet doesn't carry over. The remaining days do: the new term is added on top of the current end date.
• IPv6: the order's OWN package at your resellerPrice. An unlimited (speed) plan gets the package's days added on top of the current end date. A bandwidth plan gets the package's GB added on top of the data left, and its end date moves forward by the plan's own term.

For Dedicated ISP, Datacenter and IPv6 orders, packageId is optional and ignored: the API always uses the order's own package. Send an empty JSON body {}. On Custom ISP, leave packageId out (or send {}) to renew for the plan's own length, or send one of the packageIds from /extendOptions to renew for that length; any other packageId is refused with 400 and nothing is charged. A body is required (it must be valid JSON), and packageId is the only field it accepts.

No VAT is added on reseller balance renewals. The price is exactly packages[].price from GET /order/:id/extendOptions, so check it there first.

The response already contains the updated order (same shape as GET /order/:id), so order.proxyCredentials.expired_at is the new end date. You can also read GET /order/:id again.

Renewals are not idempotent: a second call after the first one succeeded renews, and charges, again. Datacenter, IPv6 and Custom ISP run one renewal per order at a time: a call that arrives while one is still running gets 409 and is not charged. An order whose last renewal is under review cannot be renewed until our team has finished it. If a call times out, read GET /order/:id and compare expired_at before sending it again.

READ THE MESSAGE ON A 500. "Failed to extend proxy plan. Your balance has been refunded." means nothing happened and your balance is back: safe to retry later. "Your renewal payment went through, but it needs manual review by our team..." means you WERE charged and our team applies the renewal within 24 hours or refunds it: do NOT retry. On Datacenter, IPv6 and Custom ISP a retry only gets 409 "This plan's last renewal is being checked by our team...", so it cannot charge you twice, but it will not help either.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The order ID to extend |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `packageId` | string | no | Extension package ID from /extendOptions. Required for Residential/Mobile. Custom ISP: optional, a packageId from /extendOptions renews for that package's length (left out = the plan's own length). Optional and ignored for Dedicated ISP, Datacenter and IPv6 (the order's own package is used). The body must still be JSON: send {}. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `extendOrderId` | string | ID of the created extension order. Keep it as your reference. |
| `order` | object | The renewed order, same shape as GET /order/:id. |
| `order.proxyCredentials.expired_at` | string | The new end date (ISP, Datacenter, IPv6). |
| `order.proxyCredentials.bandwidth_left` | number | Remaining bandwidth after the renewal (Residential, Mobile, IPv6 bandwidth plans; on Datacenter the full plan GB again). |
| `order.proxyCredentials.available_bandwidth` | number | Legacy alias of bandwidth_left. |

Request:

```bash
# Datacenter, IPv6, Custom ISP, Dedicated ISP: the order's own package, empty body
curl -X POST "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{}'

# Custom ISP for another length (e.g. a 1-day plan for 1 month): packageId of that length from /extendOptions
curl -X POST "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "5d2e8f14-7a3b-4c9e-b1d0-6f4a2c8e9b37" }'

# Residential / Mobile - pass packageId from /extendOptions
curl -X POST "https://catproxies.com/api/v1/public/order/8a08caa7-e1f2-487e-aedb-9ebc018ab8f0/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f" }'
```

Response:

```json
// Datacenter order, renewed for 30 days. order is the same shape as GET /order/:id
{
  "type": "response",
  "message": "Plan extended successfully! Your proxy expiration has been updated.",
  "payload": {
"extendOrderId": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
"order": {
  "id": "c5c7c6b5-e488-420c-b436-e90e86031751",
  "package_proxyType": "DatacenterP",
  "status": "CONFIRMED",
  "extendOrders": [
    { "id": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904", "type": "days", "value": "30", "amount": 100, "status": "CONFIRMED" }
  ],
  "proxyCredentials": {
    "username": "dcuser482913",
    "password": "dcpass91827",
    "total_bandwidth": 250,
    "used_bandwidth": 0,
    "bandwidth_left": 250,
    "available_bandwidth": 250,
    "expired_at": "2026-11-10T10:00:00.000Z",
    "port_http": "1338",
    "port_socks5": "1339",
    "...": "ip_list, rotating_gateways and the rest, as on GET /order/:id"
  }
}
  },
  "pagination": {}
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | Validation error | The body has a field other than packageId. |
| 400 | This plan can't be renewed with that package. Please pick one from the plan's renewal options. | Custom ISP: the packageId is not one of the options GET /order/:id/extendOptions lists for this order. Nothing was charged. |
| 400 | Insufficient balance. Required: $100.00, Available: $42.10 | Top up your reseller balance. Nothing was charged. Match on the start of the message: in rare cases it is just "Insufficient balance" without the amounts. |
| 400 | This plan can't be renewed automatically. Please contact us and we'll renew it for you. | Datacenter or IPv6 order with no package on record. Nothing was charged; contact us. |
| 400 | An IPv6 plan can hold at most 5000 GB. This package would take yours to 5250 GB, so please pick a smaller one. | IPv6 bandwidth plan: the renewal would take the plan past 5000 GB in total. Nothing was charged. |
| 403 | Cannot extend an expired proxy | The plan has already run out. Buy a new one. |
| 403 | Please add a name and address to complete your profile. This is required in order for invoices to be created. | Complete the profile on your account once. Also 403: "Plan extensions from your country are not allowed..." and "Unauthorized access to this order". |
| 404 | Order not found | No order with that id. |
| 404 | Reseller pricing not found for this package | The order's package is no longer assigned to your account. Nothing was charged; contact us. |
| 409 | A renewal for this plan is already in progress. Please wait a moment for it to finish before trying again. | Datacenter, IPv6 and Custom ISP. A renewal of this order is still running. This call was not charged. Wait, then check GET /order/:id before renewing again. |
| 409 | This plan's last renewal is being checked by our team. Please don't renew it again yet, contact us and we'll sort it out. | Datacenter, IPv6 and Custom ISP. An earlier renewal is under manual review. This call was not charged. Contact us. |
| 409 | This plan is already being reviewed by our team. Please don't renew it yet, contact us and we'll sort it out. | Dedicated ISP. The plan is under manual review. This call was not charged. Contact us. |
| 500 | Failed to extend proxy plan. Your balance has been refunded. | Nothing happened and the money is back on your balance. Safe to retry later. |
| 500 | Your renewal payment went through, but it needs manual review by our team. It will be applied within 24 hours or refunded. Please don't try again. | You WERE charged and our team finishes it. Do NOT retry. |

#### `PATCH /order/:id/whitelist`

Add an IP to a Datacenter order's whitelist. Whitelisted IPs use the proxy without username:password. 1 slot by default, 3 with the whitelisted_ips addon; accepts IPv4 or IPv6.

Only Datacenter has an IP whitelist. Every other product is username:password only.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Datacenter order ID |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ip` | string | yes | IP to whitelist, IPv4 or IPv6. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `whitelist_ip` | array | Current list of whitelisted IPs after the update |
| `slots_used` | number | Whitelist slots currently used |
| `slots_max` | number | Max slots (1 default, 3 with the whitelisted_ips addon) |

Request:

```bash
curl -X PATCH "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/whitelist" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
"ip": "203.0.113.42"
  }'
```

Response:

```json
{
  "type": "response",
  "message": "IP whitelisted successfully",
  "payload": {
"whitelist_ip": ["203.0.113.42"],
"slots_used": 1,
"slots_max": 3
  }
}
```

#### `DELETE /order/:id/whitelist`

Remove an IP from a Datacenter order's whitelist.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Datacenter order ID |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ip` | string | yes | IP to remove, IPv4 or IPv6. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `whitelist_ip` | array | Current list of whitelisted IPs after removal |
| `slots_used` | number | Whitelist slots currently used |
| `slots_max` | number | Max whitelist slots available |

Request:

```bash
curl -X DELETE "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/whitelist" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
"ip": "203.0.113.42"
  }'
```

Response:

```json
{
  "type": "response",
  "message": "IP removed from whitelist",
  "payload": {
"whitelist_ip": [],
"slots_used": 0,
"slots_max": 3
  }
}
```

#### `GET /order/:id/whitelist`

List the IPs currently whitelisted for a Datacenter order, with slots_used / slots_max.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Datacenter order ID |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `whitelist_ip` | array | IPs currently whitelisted for this order |
| `slots_used` | number | Whitelist slots in use |
| `slots_max` | number | Max slots (1, or 3 with the whitelisted_ips addon) |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/whitelist" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"whitelist_ip": ["203.0.113.42"],
"slots_used": 1,
"slots_max": 3
  }
}
```

#### `GET /order/:id/stats`

Read-only usage analytics for one Datacenter or IPv6 order: traffic and requests over time, a log of recent connections, and the sites used most. It never changes the plan and costs nothing, so it is safe to show to your customers.

Pick what you want with view:
• overview (default): uploaded, downloaded and total GB, requests and success rate over time, one row per interval.
• activity: recent connections, 100 per page. Page through with page.
• top-domains: the sites used most, with requests, success rate, bandwidth and average duration.

The figures come from the live network and lag a little behind real time. available: false means the stats source could not be reached just now; the rest of the payload is then empty, so retry later rather than reading it as zero traffic. metrics is always null on Datacenter and IPv6.

GET /order/:id/unlimited-metrics is the older name of this endpoint. It takes the same parameters and returns exactly the same thing, and keeps working for integrations that already call it.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Datacenter or IPv6 order ID |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `view` | string | no | "overview", "activity" or "top-domains". |
| `timeframe` | string | no | How far back to look: "1hour", "6hours", "1day", "1week", "1month" or "3months". Any other value falls back to "1day". |
| `interval` | string | no | overview only. Size of each row: "5min", "30min", "1hour", "6hour", "1day", "1week" or "1month". Any other value falls back to "1hour". |
| `page` | number | no | activity only. Page number, 100 connections per page. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `available` | boolean | false = the stats source could not be reached just now and the rest is empty. Retry later; it does not mean zero traffic. |
| `metrics` | object|null | overview only. Always null on Datacenter and IPv6. |
| `series[].timestamp` | string | Start of the row, as a "YYYY-MM-DD HH:MM:SS" string (a space, not a T). |
| `series[].uploaded_gb / downloaded_gb / total_gb` | number | Traffic in that row, in GB. |
| `series[].requests` | number | Requests in that row. |
| `series[].success_rate` | number | Share of successful requests, in percent (0 to 100). |
| `activity[].timestamp` | string | When the connection happened, as a "YYYY-MM-DD HH:MM:SS" string. |
| `activity[].outcome` | string | Result of the connection, for example SUCCESS or ERROR. |
| `activity[].downloaded_bytes / uploaded_bytes` | number | Traffic of that connection, in bytes. |
| `activity[].duration_ms` | number | How long the connection took, in milliseconds. |
| `activity[].client_ip` | string | The address the connection came from (the proxy user's side). |
| `pagination` | object | activity only, inside payload. total rows, per_page (100), current_page and last_page. |
| `topDomains[].success_rate` | number | Share of successful requests to that site, in percent. |
| `topDomains[].total_bandwidth / total_downloaded / total_uploaded` | number | Traffic to that site, in bytes. |
| `topDomains[].avg_duration_ms` | number | Average request duration, in milliseconds. |
| `summary` | object|null | top-domains only. Totals for the timeframe as name: number pairs, for example total_domains, total_requests, total_bandwidth (bytes) and total_bandwidth_gb. Only numbers are passed on and the set of names can change, so read them defensively. null when there is none. |

Request:

```bash
# Traffic over the last week, one row per day
curl -X GET "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/stats?timeframe=1week&interval=1day" \
  -H "Authorization: Bearer cp_your_api_key_here"

# Recent connections in the last hour, page 2
curl -X GET "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/stats?view=activity&timeframe=1hour&page=2" \
  -H "Authorization: Bearer cp_your_api_key_here"

# The sites used most today
curl -X GET "https://catproxies.com/api/v1/public/order/c5c7c6b5-e488-420c-b436-e90e86031751/stats?view=top-domains" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// view=overview (Datacenter or IPv6, so metrics is null)
{
  "type": "response",
  "message": "Success",
  "payload": {
"metrics": null,
"series": [
  {
    "timestamp": "2026-09-10 00:00:00",
    "uploaded_gb": 0.000473,
    "downloaded_gb": 0.283713,
    "total_gb": 0.284186,
    "requests": 245,
    "success_rate": 100
  },
  {
    "timestamp": "2026-09-11 00:00:00",
    "uploaded_gb": 0.000391,
    "downloaded_gb": 0.201577,
    "total_gb": 0.201968,
    "requests": 188,
    "success_rate": 98.9
  }
],
"timeframe": "1week",
"interval": "1day",
"available": true
  },
  "pagination": {}
}

// view=activity
{
  "type": "response",
  "message": "Success",
  "payload": {
"activity": [
  {
    "timestamp": "2026-09-11 08:00:56",
    "domain": "example.com",
    "outcome": "SUCCESS",
    "protocol": "SOCKS5",
    "downloaded_bytes": 228,
    "uploaded_bytes": 77,
    "duration_ms": 284,
    "client_ip": "203.0.113.77"
  }
],
"pagination": { "total": 164, "per_page": 100, "current_page": 2, "last_page": 2 },
"timeframe": "1hour",
"available": true
  },
  "pagination": {}
}

// view=top-domains
{
  "type": "response",
  "message": "Success",
  "payload": {
"topDomains": [
  {
    "domain": "example.com",
    "requests": 29,
    "success_rate": 100,
    "total_bandwidth": 242297,
    "total_downloaded": 186472,
    "total_uploaded": 55825,
    "avg_duration_ms": 145.2
  }
],
"summary": {
  "total_domains": 13,
  "total_requests": 463,
  "total_bandwidth": 284284595,
  "total_bandwidth_gb": 0.2843
},
"timeframe": "1day",
"available": true
  },
  "pagination": {}
}

// Stats source not reachable right now (retry later)
{
  "type": "response",
  "message": "Success",
  "payload": { "metrics": null, "series": [], "timeframe": "1day", "interval": "1hour", "available": false },
  "pagination": {}
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 404 | Order not found | No reseller order with that id on your account. |
| 400 | Usage statistics are available for Datacenter, IPv6 and Unlimited Residential plans only. | The order is another product. Only Datacenter and IPv6 have usage stats. |
| 400 | This plan has no proxy yet. | The plan is still being set up. Try again shortly. |
| 500 | Internal server error | Our side failed. Safe to retry. |

#### `POST /order/:id/reset-password`

Generate a new password for a Standard Residential, Premium Residential, IPv6 or Datacenter order and return it. The new password takes a few minutes to activate everywhere, and until then the old one may still work on some locations - update your integrations right away. Available for Standard Residential, Premium Residential, IPv6, Datacenter and Rotating Mobile orders. The returned password length depends on the product (Standard Residential ~36 characters, Premium Residential 8 characters, IPv6 and Datacenter 12 characters). Where the targeting flags live depends on the product: on Standard Residential, IPv6 and Datacenter they are on the USERNAME, so only the password half of your proxy strings changes. On Premium Residential they are appended to the PASSWORD, so a reset invalidates every proxy string you have built and you must re-append your flags to the new password. Rotating Mobile works differently: the plan gets a whole new login and its remaining data moves onto it, so the USERNAME changes too and the response also carries `username`. The old mobile login stops working straight away, used and total GB carry over, and a mobile plan can be reset once per hour (not within 5 minutes of a top-up). Rate limited to 3 requests per minute per reseller account. No request body.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Standard Residential, Premium Residential, IPv6, Datacenter or Rotating Mobile order ID |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `password` | string | The new proxy password (GET /order/:id returns it from now on) |
| `username` | string | Rotating Mobile only: the new proxy username (the old one stops working) |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/order/8a08caa7-e1f2-487e-aedb-9ebc018ab8f0/reset-password" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Proxy password reset successfully",
  "payload": {
"password": "the new proxy password",
"username": "only on Rotating Mobile: the new proxy username"
  }
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 429 | Too many requests. Please wait a moment. | More than 3 resets in a minute on your account. Wait a minute and try again. |
| 404 | Order not found | No reseller order with that id on your account. |
| 400 | Password reset is only available for Standard, Premium and Unlimited Residential, IPv6, Datacenter and Mobile orders | The order is another product (Custom ISP, Dedicated ISP). |
| 400 | This plan has no data left, so there is nothing to move to a new password. Top it up first, then reset. | Rotating Mobile only: a plan with 0 GB left cannot be moved to a new login. |
| 409 | This plan was just topped up. Please wait about 5 minutes, then reset the password. | Rotating Mobile only: the plan was extended in the last 5 minutes. |
| 429 | You can reset this plan's password once an hour. Please try again in 12 minutes. | Rotating Mobile only: one reset per plan per hour. |
| 502 | We couldn't reset the password right now. Nothing was changed on your plan, please try again in a few minutes. | Rotating Mobile only: the reset did not happen and the login is unchanged. Safe to retry. |
| 400 | This order has expired. | Expired plans cannot be reset. |
| 400 | This order has no proxy to reset. | The plan is still being set up. Try again shortly. |
| 502 | Failed to reset password. Please try again. | The reset did not go through. Safe to retry. |
| 500 | Internal server error | Our side failed. Check GET /order/:id before retrying. |

#### `GET /order/:id/proxies`

Get the individual proxy lines for a Custom ISP (Isp) or Dedicated ISP (IspP) order. Each order provisions several IP:port endpoints, each with its own login/password - this returns that list. ISP orders only (other products return their credentials directly on GET /order/:id); returns 400 for any other proxy type. If the order is still being provisioned right after purchase, you get HTTP 202 with an empty list - retry in a few seconds.

Custom ISP: every line uses port 1337 for both HTTP and SOCKS5, and the line format is host:port:login:password. host is a plain server IP, or your own hostname once it is set up (see /custom-isp/domain), never one of our names; read the lines each time, servers can change. The order has one password; each line's login carries its IP, and ip is the address the line exits from. Pick the connection server with ?region=: connection_regions lists them. auto (the default) is your hostname itself when you have one (closest server to whoever connects), otherwise the server IP closest to where each line's IP is. The payload also has replacements { bought, used, left, pending } and clean_score (true on a Clean Score order).

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Custom ISP (Isp) or Dedicated ISP (IspP) order ID |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `region` | string | no | Custom ISP only. The connection server: "auto" or an id from connection_regions, e.g. "us-east" or "de". An unknown value falls back to "auto". With your own hostname, auto is that hostname (closest server to whoever connects); without one, auto is the server closest to where each line's IP is. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `proxies[].host` | string | The proxy IP - connect to host:port_http (HTTP) or host:port_socks (SOCKS5). On Custom ISP it is a plain server IP for the chosen region, or your own hostname once it is set up (see /custom-isp/domain). Never save it: servers can change. |
| `proxies[].port_http` | number | HTTP port for this line |
| `proxies[].port_socks` | number | SOCKS5 and UDP-over-SOCKS5 port for this line. Not the same as port_http on Dedicated ISP, so always use this value. On Custom ISP both ports are 1337. |
| `proxies[].login` | string | Username for this line |
| `proxies[].password` | string | Password for this line |
| `auth_mode` | string | Always "logpas": use login/password. |
| `whitelist_ip` | string | Always null. |
| `quantity` | number | Number of proxy lines on the order |
| `location` | string|null | Order location. Always null for Dedicated ISP (IspP) by design, it is pool-based with no country. Custom ISP returns the city as "Country - City". |
| `stale` | boolean | true = served from the last-known-good cache during a brief upstream outage; the lines are still valid |
| `proxies[].ip` | string | Custom ISP only. The IP the line exits from. Send it to POST /order/:id/replace to swap it. |
| `connection_region` | string | Custom ISP only. The connection server these lines use. |
| `connection_regions` | array | Custom ISP only. Every connection server as { id, label }. Pass an id as ?region=. |
| `replacements` | object | Custom ISP only. { bought, used, left, pending }. pending is true while a replacement is being confirmed. |
| `clean_score` | boolean | Custom ISP only. true on a Clean Score (premium tier) order. |

Request:

```bash
# Dedicated ISP
curl -X GET "https://catproxies.com/api/v1/public/order/8a08caa7-e1f2-487e-aedb-9ebc018ab8f0/proxies" \
  -H "Authorization: Bearer cp_your_api_key_here"

# Custom ISP, through the US East connection server
curl -X GET "https://catproxies.com/api/v1/public/order/9d8e7f6a-5b4c-4d3e-8f2a-1b0c9d8e7f6a/proxies?region=us-east" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// Dedicated ISP order
{
  "type": "response",
  "message": "Success",
  "payload": {
"proxies": [
  {
    "host": "203.0.113.10",
    "port_http": 61234,
    "port_socks": 62234,
    "login": "user_a1b2c3",
    "password": "pass_d4e5f6"
  }
],
"location": "US",
"quantity": 5,
"expires_at": "2026-08-14T12:00:00.000Z",
"active": true,
"auth_mode": "logpas",
"whitelist_ip": null,
"stale": false
  }
}

// Custom ISP order (Clean Score, 2 replacements bought, 1 used)
{
  "type": "response",
  "message": "Success",
  "payload": {
"proxies": [
  {
    "host": "198.51.100.40",
    "port_http": 1337,
    "port_socks": 1337,
    "login": "cpx8f31a2-203.0.113.24",
    "password": "Rk4pX9wq",
    "ip": "203.0.113.24"
  }
],
"location": "United States - New York City",
"quantity": 5,
"expires_at": "2026-11-12T12:00:00.000Z",
"active": true,
"auth_mode": "logpas",
"whitelist_ip": null,
"stale": false,
"connection_region": "us-east",
"connection_regions": [
  { "id": "auto", "label": "Auto (closest server)" },
  { "id": "us-east", "label": "US East (Virginia)" },
  { "id": "de", "label": "Germany (Frankfurt)" }
],
"replacements": { "bought": 2, "used": 1, "left": 1, "pending": false },
"clean_score": true
  }
}
```

#### `POST /order/:id/replace`

Custom ISP only. Replaces one IP of the order with a new IP in the same city, using one of the IP replacements bought with the order (ispData.replacements on POST /order). The city and UDP support stay the same; the carrier can differ.

200 means it is done: the reply has the old IP, the new IP and how many replacements are left. Read GET /order/:id/proxies again for the new line. HTTP 202 with { checking: true } means the swap is still being confirmed: do NOT send it again. Our team checks it, and replacements.pending on GET /order/:id/proxies stays true until it is done.

One replacement at a time per order, about a minute apart. The order must be active and not expired.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | A Custom ISP order ID |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ip` | string | yes | The IP to replace, as proxies[].ip on GET /order/:id/proxies. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `oldIp` | string | The IP that was removed from the order. |
| `newIp` | string | The IP that took its place. |
| `left` | number | IP replacements left on the order. |
| `checking` | boolean | Only on HTTP 202: still being confirmed. Do not retry. |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/order/9d8e7f6a-5b4c-4d3e-8f2a-1b0c9d8e7f6a/replace" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "ip": "203.0.113.24" }'
```

Response:

```json
// HTTP 200, swapped
{
  "type": "response",
  "message": "IP replaced",
  "payload": {
"oldIp": "203.0.113.24",
"newIp": "203.0.113.88",
"left": 1
  }
}

// HTTP 202, still being confirmed. Do not retry.
{
  "type": "response",
  "message": "Your replacement is taking longer than usual. Our team is checking it and will sort it out, you don't need to do anything.",
  "payload": { "checking": true }
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | ip is required (one of the plan's IPs from GET /order/{id}/proxies). | ip is missing. |
| 400 | IP replacement is only available on Custom ISP orders. | The order is another product. |
| 400 | You have no IP replacements left on this plan. | Every replacement bought with the order has been used. |
| 400 | This IP is no longer on your plan. Refresh the page and try again. | The IP is not on the order any more. Read GET /order/:id/proxies again. |
| 400 | This plan has expired. | Expired orders cannot swap IPs. |
| 400 | We couldn't replace this IP right now, your replacement was not used. Please try again later or contact support. | Nothing changed and no replacement was used. Safe to try again later. |
| 404 | Order not found | No reseller order with that id on your account. |
| 409 | A replacement is already running on this plan. Please wait a moment. | Wait for the running one to finish. |
| 429 | Please wait a minute between two replacements. | Wait a minute, then try again. |
| 503 | We couldn't reach the IP list right now. Please try again in a minute. | Nothing changed. Try again shortly. |

#### `POST /order/:id/customer`

OPTIONAL. This endpoint is only for resellers who want to organise their orders by end customer. It does nothing to the proxy itself.

Tag or re-tag an existing order with your own customer id/label so that customer's plans group together in your panel. You can also set an optional free-text note, a group name, and tags. Call it again on the same order to change the values; pass an empty customerId to clear the tag.

You never have to use this. Resellers who do not tag orders keep buying and managing exactly as before, with no change to any request or response.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The order ID to tag |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `customerId` | string | yes | Your own id or label for the end customer. Pass an empty string to clear the tag. |
| `note` | string | no | Optional free-text note to store against the order. |
| `group` | string | no | Optional group/segment name to file this customer under. |
| `tags` | array | no | Optional array of string labels for your own filtering. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `order.id` | string | Order ID |
| `order.customerId` | string | The customer id/label now stored on the order |
| `order.note` | string|null | The stored note, if any |
| `order.group` | string|null | The stored group, if any |
| `order.tags` | array | The stored tags |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/order/700c853f-9818-4162-bed9-10003d3cb85e/customer" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
"customerId": "acme-co",
"note": "Renewed on annual plan",
"group": "EU clients",
"tags": ["priority", "scraping"]
  }'
```

Response:

```json
{
  "type": "response",
  "message": "Order tagged successfully",
  "payload": {
"order": {
  "id": "700c853f-9818-4162-bed9-10003d3cb85e",
  "customerId": "acme-co",
  "note": "Renewed on annual plan",
  "group": "EU clients",
  "tags": ["priority", "scraping"]
}
  }
}
```

#### `GET /order/custom-options`

Returns one entry per pool family you are assigned (Standard Residential, Premium Residential, Mobile) with its per-GB rate and the packageId to pass to POST /order/custom.

Custom sizing is only available for these pooled products. Fixed-size products (ISP, Datacenter, IPv6) are not listed here, buy those with POST /order at their set size.

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `options[].packageId` | string | Pass this as packageId to POST /order/custom. |
| `options[].family` | string | White-label product family name. |
| `options[].ratePerGb` | number | Your price per GB (USD). Net cost of a custom plan = gb × ratePerGb. |
| `options[].baseGb` | number | The base package size the plan is built from (informational). |
| `options[].minGb` | number | Minimum GB you can request (0.1). |
| `options[].maxGb` | number | Maximum GB per single plan. |
| `options[].defaultDays` | number | The plan length (in days) a new custom plan in this family is created with, set for your account. A trial can be shorter (see trialDays on POST /order/custom), never longer. |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/order/custom-options" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "ok",
  "payload": {
"options": [
  {
    "packageId": "6f19b4e7-1b1e-4ed5-85e4-41e6bbe07101",
    "family": "Standard Residential",
    "title": "Standard Residential",
    "ratePerGb": 1.05,
    "baseGb": 1,
    "minGb": 0.1,
    "maxGb": 100000,
    "defaultDays": 30
  }
]
  }
}
```

#### `POST /order/custom`

Create a pooled plan (Standard Residential, Premium Residential or Mobile) at an arbitrary GB size, instead of only the fixed sizes in your store. Pass the packageId from GET /order/custom-options and the GB you want.

The GB is rounded UP to the nearest 0.1. You are charged your per-GB rate for exactly the size created, so the cost = gb × ratePerGb (see the estimate in /order/custom-options). Reseller orders carry NO VAT (resellers are invoiced once, on balance deposits). Note ratePerGb is shown rounded to cents; the actual charge uses the exact unrounded rate, so pre-computed totals can differ by a cent or two. Returns the full order including proxy credentials (same shape as GET /order/:id); on the rare partial case the returned gb is null and order may be null, so read the order back from GET /order/:id.

Optionally pass customerId (and note/group/tags) to tag the new plan to one of your customers in the same call.

PLAN LENGTH: a custom plan is created for your account’s standard plan length (a fixed number of days set for your account; see defaultDays in GET /order/custom-options). To create a shorter TRIAL plan, pass trialDays with a whole number of days below that standard length. trialDays is capped at your standard length, so you can never exceed it, and the length actually used is returned as expirationDays.

RATE LIMIT: at most 1 custom-create per 10 seconds per account (a 429 with a "plan is already being created" message means back off, a create is not idempotent).

RARE CASE: if the plan is created but the provider could not size it all the way (a transient upstream issue), the response still succeeds with "partial": true and the plan set to the size we could reach, and you were only charged for that size. Add the remaining GB later with POST /order/:id/bandwidth. Do NOT re-submit: a re-submit creates a NEW plan and a NEW charge. If this endpoint returns 409, the charge STANDS and a human is finishing the order, do NOT retry (a retry buys a second plan); everything else (400/403/502) created nothing.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `packageId` | string | yes | A packageId from GET /order/custom-options (the family to build in). |
| `gb` | number | yes | Desired plan size in GB. Rounded UP to the nearest 0.1. |
| `customerId` | string | no | Optional. Tag the new plan to this customer id/label. |
| `note` | string | no | Optional note stored against the order. |
| `group` | string | no | Optional group/segment name. |
| `tags` | array | no | Optional array of string labels. |
| `trialDays` | number | no | Optional. Create a shorter TRIAL plan of this many whole days instead of your normal plan length. It is capped at your normal length (a higher number is clamped down). Omit for the normal length. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `orderId` | string | The new order id. Fetch full details any time with GET /order/:id. |
| `planId` | string|null | The provisioned plan (sub-user) id. |
| `family` | string | White-label product family. |
| `requestedGb` | number | The GB you asked for (after rounding up). |
| `gb` | number | The GB actually on the plan. Equals requestedGb unless partial is true. |
| `ratePerGb` | number | Your per-GB rate used for the charge. |
| `partial` | boolean | true if the plan could not be sized all the way (see description). |
| `trial` | boolean | true when you passed trialDays (a shorter trial plan was created). |
| `expirationDays` | number | The plan length in days actually used: your normal length, or the capped trialDays. |
| `newBalance` | number | Your reseller balance after the charge. |
| `order` | object | The full order incl. proxyCredentials (same as GET /order/:id). |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/order/custom" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
"packageId": "6f19b4e7-1b1e-4ed5-85e4-41e6bbe07101",
"gb": 25,
"customerId": "acme-co"
  }'
```

Response:

```json
{
  "type": "response",
  "message": "Custom plan created.",
  "payload": {
"orderId": "9c2b1f7a-1c2d-4e5f-9a8b-7c6d5e4f3a2b",
"planId": "sub_ab12cd34",
"family": "Standard Residential",
"requestedGb": 25,
"gb": 25,
"ratePerGb": 1.05,
"partial": false,
"trial": false,
"expirationDays": 30,
"customerId": "acme-co",
"newBalance": 148.75,
"order": { "id": "9c2b1f7a-...", "proxyCredentials": { "username": "...", "password": "..." } }
  }
}
```

#### `POST /order/:id/bandwidth`

Adjust a pooled plan (Standard Residential, Premium Residential, Mobile) after purchase. Three modes:

• "add": add GB. Charged at your per-GB rate.
• "remove": remove GB (up to the remaining amount). Refunded at your per-GB rate.
• "full": delete the whole plan; the unused GB is refunded to your balance.

Only pooled products support this. ISP, Datacenter and IPv6 plans are rejected. Money is always recomputed server-side from what you paid.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The order to adjust. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | string | yes | "add", "remove" or "full". |
| `gb` | number | no | How many GB to add/remove. Ignored for mode "full". |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `addedGb / removedGb` | number | GB added or removed. |
| `chargeAmount / refundAmount` | number | Amount charged (add) or refunded (remove/full). |
| `availableGb` | number | Remaining GB on the plan after the change. |
| `newBalance` | number | Your reseller balance after the change. |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/order/700c853f-9818-4162-bed9-10003d3cb85e/bandwidth" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "add", "gb": 5 }'
```

Response:

```json
{
  "type": "response",
  "message": "Bandwidth added.",
  "payload": {
"addedGb": 5,
"chargeAmount": 5.25,
"newBalance": 143.50,
"availableGb": 30,
"status": "CONFIRMED"
  }
}
```

#### `GET /order/:id/exchange-options`

For a pooled plan, returns the OTHER two pool families you can swap its remaining value into, with the estimated GB you would get in each. An exchange is value-preserving: it costs nothing net (you already paid for the source GB).

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The pooled order to exchange. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `source` | object | The current plan: remaining GB, its USD value and family. |
| `options[].packageId` | string | Pass this as targetPackageId to POST /order/:id/exchange. |
| `options[].estimatedGb` | number | GB you would get in that family for the same value. |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/order/700c853f-9818-4162-bed9-10003d3cb85e/exchange-options" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "ok",
  "payload": {
"source": { "availableGb": 20, "valueUsd": 21, "ratePerGb": 1.05, "family": "Standard Residential" },
"options": [
  { "packageId": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f", "family": "Mobile", "ratePerGb": 3.5, "estimatedGb": 6 }
]
  }
}
```

#### `POST /order/:id/exchange`

Swap this pooled plan's remaining value into a different pool family (from GET /order/:id/exchange-options) for the same customer. Value-preserving, the swap nets to zero. The old plan is released and a new one is created and returned; fetch its credentials with GET /order/:id using the returned targetOrderId.

The exchange is based on the bandwidth ACTUALLY REMAINING right now (read live), never the original or extended size. RATE LIMIT: 1 exchange per 30 seconds per plan (429 if one is already in progress). RARE CASE: if the old plan was already released and refunded but the new plan could not be created, you get a 502 whose message says the value was refunded to your balance, do NOT retry (the source is already gone); buy the new plan from your store instead.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The source pooled order. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `targetPackageId` | string | yes | A packageId from GET /order/:id/exchange-options. |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `targetOrderId` | string | The new order id (fetch creds with GET /order/:id). |
| `targetPlanId` | string|null | The new provisioned plan id. |
| `newGb` | number | GB on the new plan. |

Request:

```bash
curl -X POST "https://catproxies.com/api/v1/public/order/700c853f-9818-4162-bed9-10003d3cb85e/exchange" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "targetPackageId": "6f19b4e7-1b1e-4ed5-85e4-41e6bbe07101" }'
```

Response:

```json
{
  "type": "response",
  "message": "Exchange complete.",
  "payload": {
"sourceOrderId": "700c853f-9818-4162-bed9-10003d3cb85e",
"targetOrderId": "9c2b1f7a-64d0-4a1e-9f3b-2c8a5d0e7b41",
"targetPlanId": "sub_ab12cd34",
"targetFamily": "Mobile",
"exchangedGb": 20,
"newGb": 6,
"valueUsd": 21,
"newBalance": 148.75
  }
}
```

### Targeting

#### `GET /gresi-targeting-options`

Get available targeting options for Standard Residential proxies. This endpoint is only available for Standard Residential proxy orders.

Response shape: a single "countries" array. ASNs, states and cities are all nested inside their country - there is no top-level "asns" list, and cities live under states (countries[].states[].cities[]), not directly under the country. Use the "code" of each entry in the flags below; codes are lowercase.

This payload is large (~2 MB, 214 countries, 1400+ ASNs for the US alone) and rarely changes - fetch it once and cache it rather than calling it per request.

Connecting: use the host from GET /order/:id (HTTP ports 9000 rotating / 10000 sticky, SOCKS5 11000 / 12000) and append targeting to the USERNAME: -type-residential (required), -country-us, -state-<s>, -city-<c>, -asn-<n>. For a sticky IP add -lifetime-<minutes>-session-<id> (hold up to 1440 minutes = 24h); omit the sticky flags for a fresh IP each request.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | Order ID of a Standard Residential proxy order |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `countries[].code` | string | Country code, lowercase ISO-2 (use in -country-) |
| `countries[].name` | string | Country display name |
| `countries[].asns[].code` | string | ASN number only, no "AS" prefix (use in -asn-) |
| `countries[].asns[].name` | string | ASN display name, prefixed with its number |
| `countries[].states[].code` | string | State code, lowercase full name, no spaces (use in -state-) |
| `countries[].states[].name` | string | State display name |
| `countries[].states[].cities[].code` | string | City code, lowercase, no spaces or separators (use in -city-) |
| `countries[].states[].cities[].name` | string | City display name |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/gresi-targeting-options?orderId=8a08caa7-e1f2-487e-aedb-9ebc018ab8f0" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"countries": [
  {
    "code": "us",
    "name": "United States",
    "asns": [
      { "code": "7922", "name": "AS7922 Comcast Cable Communications, LLC" }
    ],
    "states": [
      {
        "code": "california",
        "name": "California",
        "cities": [
          { "code": "eastlosangeles", "name": "East Los Angeles" }
        ]
      }
    ]
  }
]
  }
}
```

#### `GET /mobile-targeting-options/countries`

Returns all countries available for Rotating Mobile targeting.

Connecting: use the host/port from GET /order/:id and append targeting to the USERNAME: -country-us, -region-<r>, -city-<c>, -isp-<i>. For a sticky IP add -sid-<id>-ttl-<duration>, where duration is 1m, 5m, 10m, 30m, 60m, 120m, 180m or 240m (hold up to 4h); omit the sticky flags for a fresh IP each request.

Cache this response - the list rarely changes (recommended TTL: 1 hour). Rate limited to 60 requests per 10 minutes per API key, shared across all 4 mobile targeting endpoints. For bulk setup, use the offline snapshot instead.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | Order ID of a Rotating Mobile proxy order |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `countries[].code` | string | Country code (lowercase) |
| `countries[].name` | string | Country display name |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/mobile-targeting-options/countries?orderId=YOUR_ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"countries": [
  { "code": "us", "name": "United States" },
  { "code": "gb", "name": "United Kingdom" },
  { "code": "de", "name": "Germany" }
]
  }
}
```

#### `GET /mobile-targeting-options/regions`

Returns available regions (states/provinces) for a given country. Cache by country code (recommended TTL: 1 hour).

Rate limited to 60 requests per 10 minutes per API key, shared across all 4 mobile targeting endpoints.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | Order ID of a Rotating Mobile proxy order |
| `country` | string | yes | Country code (e.g. "us") |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `regions[].code` | string | Region code (lowercase full name, spaces as "+", e.g. "new+york") |
| `regions[].name` | string | Region display name |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/mobile-targeting-options/regions?orderId=YOUR_ORDER_ID&country=us" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"regions": [
  { "code": "california", "name": "California" },
  { "code": "new+york", "name": "New York" },
  { "code": "texas", "name": "Texas" }
]
  }
}
```

#### `GET /mobile-targeting-options/cities`

Returns available cities within a country + region. Cache by country+region key (recommended TTL: 1 hour).

Rate limited to 60 requests per 10 minutes per API key, shared across all 4 mobile targeting endpoints.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | Order ID of a Rotating Mobile proxy order |
| `country` | string | yes | Country code (e.g. "us") |
| `region` | string | yes | Region code from /regions (e.g. "california") |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `cities[].code` | string | City code (lowercase, spaces as "+", e.g. "los+angeles") |
| `cities[].name` | string | City display name |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/mobile-targeting-options/cities?orderId=YOUR_ORDER_ID&country=us&region=california" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"cities": [
  { "code": "los+angeles", "name": "Los Angeles" },
  { "code": "san+francisco", "name": "San Francisco" },
  { "code": "san+diego", "name": "San Diego" }
]
  }
}
```

#### `GET /mobile-targeting-options/isps`

Returns available ISPs for a given country, optionally filtered by region and/or city. Cache by the combination of params used (recommended TTL: 1 hour).

Rate limited to 60 requests per 10 minutes per API key, shared across all 4 mobile targeting endpoints.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | Order ID of a Rotating Mobile proxy order |
| `country` | string | yes | Country code (e.g. "us") |
| `region` | string | no | Region code to narrow results (optional) |
| `city` | string | no | City code to narrow results (optional) |

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `isps[].code` | string | ISP identifier code |
| `isps[].name` | string | ISP display name |

Request:

```bash
curl -X GET "https://catproxies.com/api/v1/public/mobile-targeting-options/isps?orderId=YOUR_ORDER_ID&country=us&region=california&city=los+angeles" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Success",
  "payload": {
"isps": [
  { "code": "at&t+wireless", "name": "at&t wireless" },
  { "code": "t-mobile+usa", "name": "t-mobile usa" },
  { "code": "verizon+wireless", "name": "verizon wireless" }
]
  }
}
```

## White labeling

Every reseller order already returns a **plain IP** as the host, never one of our
hostnames, so nothing in the credentials you pass on points back to us. This section is
for going further and giving your own customers a hostname on YOUR domain.

### Our gateways

| Product | Gateway hostname | Ports |
| --- | --- | --- |
| Standard Residential | `resi-us` / `resi-fr` / `resi-sg` `.catproxies.com` | 9000 HTTP rotating, 10000 HTTP sticky, 11000 / 12000 SOCKS5 |
| Premium Residential | `presix` / `presix-eu` / `presix-asia` (HTTP), `presix-socks` / `presix-eu-socks` / `presix-asia-socks` (SOCKS5), all `.catproxies.com` | 6011 / 6012 / 6013 HTTP, 6014 / 6015 / 6016 SOCKS5 |
| Premium Residential ISP pools | `usax` / `usax-tk` / `ukx` / `dex` / `dex-dtag` / `aux` `.catproxies.com`, or the `isp_pools` IPs on `GET /order/:id` | 6000 / 6001 / 6003 / 6006 / 6007 / 6002, HTTP only |
| Rotating Mobile | `mobile-us` / `mobile-eu` / `mobile-asia` `.catproxies.com` | 5000 |
| IPv6 | `ipv6-ww` / `ipv6-us` / `ipv6-de` / `ipv6-gb` / `ipv6-nl` `.catproxies.com` | 1338 HTTP, 1339 SOCKS5 |
| Datacenter | No hostname. Point your record at the `rotating_gateway_sets` IPs for the country on `GET /order/:id` (for the US, all of them, so it reaches every US IP). | 1338 HTTP, 1339 SOCKS5 |
| Dedicated ISP | No gateway. You get individual IP:port lines, and they are yours for the life of the plan. | As returned |
| Custom ISP | A plain server IP per order, or your own hostname with Auto included: set it up with `POST /custom-isp/domain` (see the Custom ISP guide). Use that instead of a CNAME or A records. | 1337, HTTP and SOCKS5 |

### Two ways to point your domain at us

**CNAME. Never breaks.**

```
proxy.yourdomain.com  CNAME  resi-us.catproxies.com
```

When we move a gateway your hostname follows on its own and you do nothing. The
catch: a DNS lookup on your record shows our hostname, so anyone curious can see
who is behind it.

**A records. Nothing links back.**

```bash
# read the current IPs, then add one A record per IP
dig +short resi-us.catproxies.com

proxy.yourdomain.com  A  192.0.2.10
proxy.yourdomain.com  A  192.0.2.11
```

A lookup shows only IP addresses, so there is nothing to trace. The catch is that it
goes stale: when we rotate a gateway your record points at a dead address and your
customers break until you change it yourself.

**If you go the A record route:** add EVERY IP the hostname resolves to, not just
the first one. Most of our gateways are several nodes behind one name, and pinning a
single IP throws away the failover. Gateway IPs do change, and we announce every
change in our update channel before it happens (Discord https://discord.gg/catproxy
and Telegram https://t.me/catproxies), so follow one and update your records
when we do. If uptime matters to you more than being untraceable, use the CNAME.

## Support

Questions about the API: support@catproxies.com. The human version of this document is at https://catproxies.com/docs
