# CatProxies User API (BETA)

Documentation for the CatProxies **User API**: the developer API for an ordinary
customer who wants to buy and manage their own proxy plans from code.

**This API is in BETA.** It works and it is safe to build on, but details may still
change while we settle it in, so check back if something behaves differently.

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

- Base URL: `https://catproxies.com/api/v1/user`
- Auth: `Authorization: Bearer cp_your_api_key_here`
- Keys are created at https://catproxies.com/dashboard/my-account/api
- Everything is priced at the normal retail prices and paid from the account balance.

If you are an AI assistant helping someone integrate this API, everything you need
is in this file. Pay particular attention to *Buying is not idempotent* and to the
per-product gateway hosts, which are the two things people most often get wrong.

## What this API is

An API key does exactly what the account owner can do in the dashboard: browse the
catalogue, buy a plan from their balance, read the proxy credentials, renew a plan,
rotate a password. It is the same account and the same prices.

It is NOT a reseller account. There are no sub-customers, no wholesale pricing and no
separate panel. The reseller programme is a different product with its own API.

A key only reaches the endpoints listed in this document. Account settings, password,
two-factor and session endpoints are not reachable with a key at all, and a key never
carries admin privileges even if the account happens to be an admin.

## Authentication

Send the key as a bearer token on every request:

```
Authorization: Bearer cp_your_api_key_here
```

- Keys start with `cp_` and stay visible in the dashboard, so they can be re-read later.
- Up to five keys per account. Disabling or deleting one takes effect immediately.
- A key can spend the account balance. Treat it like a password.

## How you pay

Everything bought through the API is paid from the account balance, so send
`"paymentMethod": "Balance"` on purchases and renewals. There is no browser in the
loop, so a card or crypto checkout is rejected rather than left half finished.

Top up at https://catproxies.com/dashboard/deposit-balance and check the figure with
`GET /account`. VAT is applied exactly as it is on the website.

### Buying is not idempotent (important)

A purchase sets up a live plan and charges the balance. If a call times out or the
outcome is unclear, look the order up with `GET /orders` before retrying. Retrying
blindly buys a second plan.

There is one error that MUST be special-cased. If the response message begins
"Your payment went through and the order was created, but setting the plan up did not
finish", then the balance HAS been charged and the order exists. The payload carries
`OrderId`. Do not order again; the order is completed or refunded on our side.

## Quick start

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

# 1. Check the balance
curl -s -H "Authorization: Bearer $KEY" "$API/account"

# 2. Find a plan to buy
curl -s -H "Authorization: Bearer $KEY" "$API/packages?proxyType=v2resi&pageSize=50"

# 3. Buy it (paid from the account balance). Returns payload.OrderId.
curl -s -X POST "$API/purchase" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID_FROM_STEP_2", "paymentMethod": "Balance" }'

# 4. Read the credentials
curl -s -H "Authorization: Bearer $KEY" "$API/orders/ORDER_ID_FROM_STEP_3"

# 5. Use them. The HOST is a fixed gateway that belongs to the PRODUCT,
#    not to the order. See the per-product section below.
curl -x resi-us.catproxies.com:9000 \
  -U "USERNAME-type-residential-country-us:PASSWORD" https://ipinfo.io
```

### The single most common mistake

`proxy.server_ip` on `GET /orders/:id` is NOT the host to connect to. It is null on
most products. The host is a fixed gateway per product, listed in the per-product
sections below. Custom ISP and Dedicated ISP are the exception: there each
IP is its own line with its own host, port and login, all from `GET /generator/isp`.

## 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 a `Retry-After` header.

On top of that, these three share ONE bucket of 3 requests per minute, counted per
account rather than per key, and the account owner's own dashboard clicks count too:

| Endpoint | Limit |
| --- | --- |
| `POST /purchase` | 3 per minute (shared) |
| `POST /purchase/extend` | 3 per minute (shared) |
| `POST /orders/:id/reset-password` | 3 per minute (shared) |

Nothing is created or charged when that limit refuses a call. Put a short pause
between orders rather than firing them in parallel.

## Errors

Errors are JSON with a readable `message`, usually alongside `"type": "error"`.

One exception worth coding for: a bad or inactive API key (401), an endpoint an
API key may not use (403) and the hourly key budget (429) answer with
`{"status": 401, "statusText": "..."}` instead. The readable text is in
`statusText`, not `message`, so read both fields when you log an error.

**Branch on the HTTP status, not on `type`.** A few handled failures answer 200
with an error body, and a few 404s still carry `type: "response"`, so the status
is the only reliable signal.

| Status | Meaning | What to do |
| --- | --- | --- |
| 400 | The request was wrong | Read `message`. Usually a missing or invalid field. |
| 401 | Bad or disabled key | Check the key is correct and still active. |
| 403 | Not available to API keys | That endpoint is dashboard only. |
| 404 | Not found | The order or package id does not exist on this account. |
| 409 | A renewal is running or under review | Datacenter, IPv6 and Custom ISP renewals. Nothing was charged for this request. Read the message: wait for the running renewal, 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 a purchase or a renewal: read the message first (see below), and check /orders or /orders/:id before sending it again. |

### Messages worth handling in code

| Message | What it means |
| --- | --- |
| Insufficient balance | Top up and retry. Nothing was created. |
| Your payment went through and the order was created, but setting the plan up did not finish | The balance HAS been charged and the order exists. `payload.OrderId` identifies it. Do NOT retry. |
| Please add a name and address to complete your profile | The account needs a name and address before it can buy, because invoices are per order. |
| API orders are paid from your account balance | A paymentMethod other than "Balance" was sent. |
| This plan type cannot be renewed at the moment | Plans on a retired product. Buy a new plan instead. |
| A renewal for this plan is already in progress | HTTP 409, Datacenter, IPv6 and Custom ISP. A second renewal was sent while the first was still running. It was not charged. Wait for the first to finish and check GET /orders/:id before renewing again. |
| This plan's last renewal is being checked by our team | HTTP 409, Datacenter, IPv6 and Custom ISP. An earlier renewal is under manual review. Nothing was charged for this request. Do not renew again yet; contact us. |
| Failed to extend proxy plan. Your balance has been refunded. | HTTP 500 on a renewal. Nothing happened and the money is back on the balance. Safe to try again later. |
| Your renewal payment went through, but we need to confirm it with a quick manual check | HTTP 500, Datacenter, IPv6 and Custom ISP. The balance WAS charged; our team applies the renewal within 24 hours or refunds it. Do NOT retry. A retry only gets the 409 above, so it cannot charge twice. |
| You can only extend with a Datacenter plan of the same size | The packageId has a different IP count or bandwidth from the Datacenter plan. Pick one from `GET /orders/:id/extend-options`. |
| You can only renew this IPv6 plan with... | The packageId is the wrong kind for the IPv6 plan (unlimited vs bandwidth, or a different speed or length). Pick one from `GET /orders/:id/extend-options`. |
| An IPv6 plan can hold at most 5000 GB... | The bandwidth package would take the plan past 5000 GB in total. Nothing was charged. Pick a smaller one from `GET /orders/:id/extend-options`. |
| Please choose one of the available quantity options for this plan | Dedicated ISP. The quantity must match one of the offered sizes. |
| This city is out of stock right now / This carrier is out of stock in this city right now / Only 3 IPs are available for this choice right now | Custom ISP. Stock ran out since the catalog was read. Nothing was charged. Read `GET /custom-isp/catalog` (or `/carriers`) again and pick another city, carrier or a lower quantity. |
| You have no IP replacements left on this plan | Custom ISP. Every replacement bought with the plan has been used. |
| Package not found | On a Custom ISP purchase, send a packageId from `GET /custom-isp/catalog`, not the `customisp` marker from `/packages`. On a Datacenter or IPv6 renewal it means packageId is missing, unknown or no longer on sale. |
| Not enough IPs available | Datacenter. Allocate within the stock from `/packages/datacenterp-countries`. |
| Too many requests (HTTP 429) | The shared 3-per-minute bucket. Wording differs between endpoints, so branch on the 429 status, not the text. Nothing was created. |
| Rate limit exceeded | The hourly budget for this key is used up. |

Errors returned by a proxy while using it, rather than by this API: `407` is bad
credentials or a non-whitelisted address, and `429` from a proxy means too many
connections at once.

## Products

### Products and their proxyType

Every plan has a `proxyType`. Use it to filter the catalogue (`/packages?proxyType=...`) and to tell which product an order is. Each one has its own guide further down this page.

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

Custom ISP plans, prices and cities come from `GET /custom-isp/catalog`, not from `/packages`.

### Connecting, per product

Buying a plan is the same call for every product. What differs is the extra body the purchase needs, the gateway and the ports you connect to, and where the targeting flags go. Each guide below is complete on its own: find your product, and everything you need is in that one block.

`GET /orders/:id` gives you the credentials for a plan, as `proxy.username` and `proxy.password`. The **host** is a fixed gateway that belongs to the product, not to your order, and each guide lists them. Do not build your proxy string from `proxy.server_ip`: it is only populated on some products and is not the host you should connect to.

The exceptions are Custom ISP and Dedicated ISP, where every IP is its own line with its own host, port and login, all of which come from `GET /generator/isp`.

### What can be renewed

Renewing keeps an existing plan alive rather than creating a new one, so the credentials stay the same. Not every product supports it right now.

| Product | Renewable | What to send |
| --- | --- | --- |
| Standard Residential | Yes | orderId, paymentMethod and the packageId of the size you want |
| Premium Residential | Yes | orderId, paymentMethod and the packageId of the size you want |
| Rotating Mobile | Yes | orderId, paymentMethod and the packageId of the size you want |
| Custom ISP | Yes | orderId and paymentMethod. It renews the same plan (same length, same IPs) at its per-IP price, plus the carrier fee per IP when the plan has a carrier. Add length to renew for another length, for example a 1-day plan for 1 month. IP replacements are not renewed. Check the cost of each length with GET /purchase/isp-extend-price. |
| Dedicated ISP | Yes | orderId and paymentMethod only. It renews the same IPs for another 30 days. |
| Datacenter | Yes | orderId, paymentMethod and the packageId of a package with the same IPs and bandwidth; only the length may differ. Get it, and the exact price, from GET /orders/:id/extend-options. 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, but your remaining days do. Add-ons are charged again at +25% each. |
| IPv6 | Yes | orderId, paymentMethod and the packageId of a plan of the same kind, from GET /orders/:id/extend-options: for unlimited plans the unlimited package with the same speed and the same length, a bandwidth package for bandwidth plans (its GB are added, and the plan can hold at most 5000 GB in total). |

Whatever you send, the amount charged and the bandwidth or days added are always worked out on our side from the package itself, never from the request body. Anything else you put in the body is ignored.

The full renewal rules for each product are in the Renewing part of its guide below.

### Country, state and city lists

Where to get the list of places you can target, per product. Some are a live
endpoint, some are one downloadable file, and two products have no list because
the country you get IS the gateway you connect to.

| Product | What you can target | Where the list comes from |
| --- | --- | --- |
| Standard Residential | Country, US state, city, ASN | Live: GET /generator/budget/targeting-options?orderId=<your order>. Returns the countries available to your plan, with their states and cities. |
| Premium Residential | 196 countries, 50 US states, cities in 201 countries | Download: https://catproxies.com/files/premium_residential_x_targeting.json. It carries the whole reference in one file: countries, us_states and a cities map keyed by country code, plus the exact flag format and the ports. |
| Rotating Mobile | Country, region or state, city, carrier | Live, four endpoints: GET /generator/mobile-locations/countries, then /regions, /cities and /isps for the place you picked. Offline copy of the same tree: https://catproxies.com/files/mobile_targeting.json (242 countries). |
| Custom ISP | City, and optionally the carrier and UDP support, chosen when you buy | Live: GET /custom-isp/catalog?tier=...&udp=... lists the countries and cities in stock with IP counts. GET /custom-isp/carriers?location=... lists the carriers (ASNs) in one city. |
| Dedicated ISP | None | Pool based, so there is no country to choose and locations[] is empty. |
| Datacenter | US, CA, DE, GB, NL | Fixed set of five. Live stock per country: GET /packages/datacenterp-countries. You split your IPs across them at purchase with datacenterPData.country_proxies. |
| IPv6 | Worldwide, US, DE, GB, NL | Fixed set of five, and the country IS the gateway you connect to: ipv6-ww, ipv6-us, ipv6-de, ipv6-gb, ipv6-nl .catproxies.com. Same credentials on all of them. |

Things that trip people up:

- **Standard Residential:** Values go on the USERNAME, lowercase, with no spaces or separators: -country-us-state-california-city-losangeles. It is scoped to your own order, so ask for it once at startup and cache it.
- **Premium Residential:** Values go on the PASSWORD, and the first flag joins with a dash while every one after it joins with an underscore. Multi-word cities have no separator at all: _city-losangeles.
- **Rotating Mobile:** Each entry is an object with a code and a name: use the code. Multi-word values join with a plus sign, so -city-los+angeles and -isp-at&t+wireless. The four endpoints share 60 requests per 10 minutes, so fetch them once and cache.
- **Custom ISP:** Send the city exactly as the catalog returns it ("Country - City") as ispData.location, and a carrier's asn as ispData.asn. Ask with the same tier and udp you will buy, since stock differs per tier and per UDP choice. Fixed at purchase; an IP replacement stays in the same city.
- **Datacenter:** The split must add up to exactly the IP count of the package. A worldwide rotating line only works on a plan that owns IPs in all five.
- **IPv6:** Nothing to buy per country and nothing to send: point at a different gateway and you are in a different country.

### Standard Residential (`v2resi`)

*Real home IPs, sold by the gigabyte.*

#### What it is

Sold by bandwidth. Buy it with nothing but the packageId. One set of credentials covers every country and session; you choose where an IP comes from with flags on the username. A fresh IP on every request by default.

#### Buying

```bash
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID", "paymentMethod": "Balance" }'
```

#### Proxy format and connecting

GET /orders/:id gives you proxy.username and proxy.password. The host is one of the three fixed regional gateways below: pick the one nearest your target for lower latency. The same credentials work on all of them. Ignore proxy.server_ip on this product; it is not set.

| Gateway | Rotating port | Sticky port |
| --- | --- | --- |
| resi-us.catproxies.com (United States) | 9000 HTTP / 11000 SOCKS5 | 10000 HTTP / 12000 SOCKS5 |
| resi-fr.catproxies.com (Europe) | 9000 HTTP / 11000 SOCKS5 | 10000 HTTP / 12000 SOCKS5 |
| resi-sg.catproxies.com (Asia) | 9000 HTTP / 11000 SOCKS5 | 10000 HTTP / 12000 SOCKS5 |

**Example**

```bash
# From GET /orders/:id
#   username: mtpeqbcevnemewc201120   password: jonxybfeuu
# Host: pick the gateway nearest your target from the table above

# Rotating IP in the US, a new one on every request (HTTP port 9000)
curl -x resi-us.catproxies.com:9000 \
  -U "mtpeqbcevnemewc201120-type-residential-country-us:jonxybfeuu" \
  https://ipinfo.io

# The same IP in Los Angeles for 30 minutes (sticky HTTP port 10000)
curl -x resi-us.catproxies.com:10000 \
  -U "mtpeqbcevnemewc201120-type-residential-country-us-state-california-city-losangeles-lifetime-30-session-ab12:jonxybfeuu" \
  https://ipinfo.io

# SOCKS5, rotating, through the Europe gateway
curl -x socks5h://resi-fr.catproxies.com:11000 \
  -U "mtpeqbcevnemewc201120-type-residential-country-de:jonxybfeuu" \
  https://ipinfo.io
```

#### Targeting, appended to the username

Leave the password alone and append flags to the username. -type-residential is required; everything else is optional.

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

- Without the sticky flags you get a new IP on every request. Add -lifetime-<minutes>-session-<id> to hold one IP, up to 1440 minutes (24 hours). Use a different session id per line to hold several sticky IPs at once.
- Valid countries, states, cities and ASNs come from GET /generator/budget/targeting-options?orderId=... Cache that list; it is large and changes rarely.
- If a very specific combination returns nothing, that spot is momentarily empty. Broaden the targeting: drop the city first, then the state.

#### Renewing

Yes. POST /purchase/extend with the orderId and the packageId of the size you want.

#### Good to know

- **Where these hosts come from:** These gateways are fixed, and the same list is on the plan page in your dashboard, which is always current. If we ever move a gateway the hostname stays the same, so build your proxy strings from the names above rather than resolving one to an IP and hard-coding that.
- **Bandwidth:** proxy.bandwidth_left on GET /orders/:id is what you have left, in GB. When it hits zero the plan stops passing traffic even if the date has not been reached.
- **Authentication:** Username and password only. There is no IP whitelist on this product.
- **Rotate the password:** POST /orders/:id/reset-password. Targeting lives on the username here, so only the password half of your proxy strings changes.

### Premium Residential (`resix`)

*A cleaner residential pool for the sites that keep blocking you.*

#### What it is

Sold by bandwidth and bought exactly like Standard Residential, with nothing but the packageId. Two things differ: the flags go on the password rather than the username, and you pick your region by which gateway you connect to.

#### Buying

```bash
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID", "paymentMethod": "Balance" }'
```

#### Proxy format and connecting

GET /orders/:id gives you proxy.username and proxy.password. The host is one of the regional gateways below, and unlike our other products the HTTP and SOCKS5 gateways are different names, so take the row for the protocol you want. The same credentials work on all of them, including the six dedicated ISP pools at the bottom of the table.

| Region | HTTP gateway | SOCKS5 gateway |
| --- | --- | --- |
| Americas | presix.catproxies.com:6011 | presix-socks.catproxies.com:6014 |
| Europe | presix-eu.catproxies.com:6012 | presix-eu-socks.catproxies.com:6015 |
| Asia, Africa and Oceania | presix-asia.catproxies.com:6013 | presix-asia-socks.catproxies.com:6016 |
| ISP pool: United States | usax.catproxies.com:6000 | Not available (HTTP only) |
| ISP pool: United States (Tickets) | usax-tk.catproxies.com:6001 | Not available (HTTP only) |
| ISP pool: United Kingdom | ukx.catproxies.com:6003 | Not available (HTTP only) |
| ISP pool: Germany | dex.catproxies.com:6006 | Not available (HTTP only) |
| ISP pool: Germany (DTAG) | dex-dtag.catproxies.com:6007 | Not available (HTTP only) |
| ISP pool: Australia | aux.catproxies.com:6002 | Not available (HTTP only) |

**Example**

```bash
# From GET /orders/:id
#   username: hftjvbtwgxxxceb205604   password: ngnstzylne
# Host: the gateway for your region and protocol, from the table above

# Worldwide, rotating (bare password, Americas HTTP gateway)
curl -x presix.catproxies.com:6011 \
  -U "hftjvbtwgxxxceb205604:ngnstzylne" https://ipinfo.io

# UK, London, with the quality filter (Europe HTTP gateway).
# First flag joins with "-", the rest with "_".
curl -x presix-eu.catproxies.com:6012 \
  -U "hftjvbtwgxxxceb205604:ngnstzylne-country-gb_city-london_streaming-1" \
  https://ipinfo.io

# Two different sticky IPs: one unique 8-character session id per line
curl -x presix.catproxies.com:6011 -U "hftjvbtwgxxxceb205604:ngnstzylne-country-us_session-a1b2c3d4_lifetime-1h" https://ipinfo.io
curl -x presix.catproxies.com:6011 -U "hftjvbtwgxxxceb205604:ngnstzylne-country-us_session-e5f6g7h8_lifetime-1h" https://ipinfo.io

# SOCKS5 uses its own gateway name and port
curl -x socks5h://presix-socks.catproxies.com:6014 \
  -U "hftjvbtwgxxxceb205604:ngnstzylne-country-us" https://ipinfo.io

# Device targeting (best effort), always the LAST flag
curl -x presix-eu.catproxies.com:6012 \
  -U "hftjvbtwgxxxceb205604:ngnstzylne-country-de_session-a1b2c3d4_lifetime-1h_device-windows" \
  https://ipinfo.io

# Dedicated ISP pool (UK): HTTP only, no country or other flags, sticky session optional
curl -x ukx.catproxies.com:6003 \
  -U "hftjvbtwgxxxceb205604:ngnstzylne-session-a1b2c3d4_lifetime-1h" https://ipinfo.io
```

#### Targeting, appended to the password

Leave the username alone and append flags to the password. The first flag joins the password with a hyphen and every flag after it with an underscore. All of them are optional; a bare password gives you a worldwide IP.

| Target | Flag | Value format |
| --- | --- | --- |
| Country | `-country-us` | ISO-2 code, lowercase. Leave it out for worldwide. |
| US state | `_state-california` | lowercase, no spaces (newyork). US only, and not with a city. |
| City | `_city-losangeles` | lowercase, no spaces. Needs a country, and not with a state. |
| Sticky session | `_session-a1b2c3d4` | exactly 8 lowercase letters or digits. One id means one IP. |
| Session lifetime | `_lifetime-1h` | how long that 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. |

- Mind the joiners: the FIRST flag attaches to the password with a hyphen, every one after it with an underscore.
- City and state are mutually exclusive and both need a country. Use one or the other, never both.
- Without a _session- flag you get a fresh IP for every new connection (a connection your client keeps open keeps its IP). Add a unique 8-character session id per line, optionally with _lifetime-, to hold a batch of different sticky IPs.
- The three filters are best effort, not guarantees. Stacking several of them narrows the pool, so use only the one you actually need.
- Device targeting is best effort too. It 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 work: any other value makes the connection hang with no error.
- The dedicated ISP pools take no flags except a sticky session: send the bare password (rotating) or PASSWORD-session-a1b2c3d4_lifetime-1h. Country, city, state, filters and device do not apply there.
- Any city in the world can be asked for, but the pool moves, so a very small city may fall back to the wider region. Prefer larger cities.

#### Renewing

Yes. POST /purchase/extend with the orderId and the packageId of the size you want.

#### Good to know

- **Where these hosts come from:** These gateways are fixed, and the same list is on the plan page in your dashboard, which is always current. If we ever move a gateway the hostname stays the same, so build your proxy strings from the names above rather than resolving one to an IP and hard-coding that.
- **Dedicated ISP pools:** Six fast, country-locked ISP pools (United States, United States Tickets, United Kingdom, Germany, Germany DTAG, Australia) come with every Premium Residential plan, on the same login and bandwidth. They are part of Premium Residential, not the separate Dedicated ISP product. The host decides the pool and its country. They are HTTP only and take no targeting flags. They are also in the Dedicated ISP pool dropdown on the plan page.
- **Bandwidth:** proxy.bandwidth_left on GET /orders/:id is what you have left, in GB.
- **Authentication:** Username and password only. There is no IP whitelist on this product.
- **Rotate the password:** POST /orders/:id/reset-password changes the BASE password only. Rebuild your strings with the new base and re-append the same flags, since targeting lives on the password here.

### Custom ISP (`Isp`)

*ISP IPs in the city you pick, on the carrier you pick.*

#### What it is

ISP proxies sold per IP, where you choose 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 if you want, the carrier and UDP support. You can also buy up to 5 IP replacements with the plan, to swap an IP later. Price = price per IP x IPs, plus the carrier fee per IP x IPs when you pick a carrier, plus replacements x the replacement price. VAT is added on top. The plans and cities come from GET /custom-isp/catalog, not from /packages. Optional: send a country instead of a city for IPs from any city in it, and ip2locationIsp (beta) to only get IPs that IP2Location marks as "ISP", a home or fixed internet line (checked right before delivery, no extra cost). If no IP can be delivered, the order retries by itself for about 12 minutes and is then refunded to your balance automatically.

#### Buying

```bash
# 1. Plans, prices and cities in stock (tier=premium for Clean Score, udp=1 for UDP only)
curl "https://catproxies.com/api/v1/user/custom-isp/catalog?tier=standard" \
  -H "Authorization: Bearer cp_your_api_key_here"
# -> plans.standard["1_month"].packageId, pricePerIp, minQty, maxQty, replacementPrice
# -> countries[].cities[].location, e.g. "United States - New York City"

# 2. Optional: carriers in that city (same tier and udp as you will buy)
curl -G "https://catproxies.com/api/v1/user/custom-isp/carriers" \
  --data-urlencode "location=United States - New York City" \
  -H "Authorization: Bearer cp_your_api_key_here"
# -> carriers[].asn, e.g. 7922

# 3. Buy 5 IPs on that carrier, with 2 IP replacements
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "packageId": "PACKAGE_ID_FROM_STEP_1",
    "paymentMethod": "Balance",
    "ispData": {
      "quantity": 5,
      "location": "United States - New York City",
      "asn": 7922,
      "udp": false,
      "replacements": 2
    }
  }'
```

#### Proxy format and connecting

GET /generator/isp?orderId=... returns one line per IP. Every line uses the same connection server and the same port, 1337, for both HTTP and SOCKS5. The plan has one password, and each line's login carries its IP, so build each proxy string from its own line. Pick the connection server with &region=: auto (the default) is the one closest to you, and connection_regions in the reply lists the others. The exit IP does not change with the region, only the route to it.

| Field | Meaning |
| --- | --- |
| host | the connection server for the region you picked (the same on every line) |
| port_http / port_socks | both 1337. HTTP, HTTPS and SOCKS5 all run on the same port. |
| login / password | the login is different on every line (it carries the line's IP), the password is the same |
| ip | the IP this line exits from. Send it to the replace endpoint to swap it. |

**Example**

```bash
# From GET /generator/isp?orderId=...&region=us-east
#   { "host": "us-east.isp.catproxies.com", "port_http": 1337, "port_socks": 1337,
#     "login": "cpx8f31a2-203.0.113.24", "password": "Rk4pX9wq", "ip": "203.0.113.24" }

# HTTP and HTTPS
curl -x us-east.isp.catproxies.com:1337 -U "cpx8f31a2-203.0.113.24:Rk4pX9wq" https://ipinfo.io

# SOCKS5, same port
curl -x socks5h://us-east.isp.catproxies.com:1337 -U "cpx8f31a2-203.0.113.24:Rk4pX9wq" https://ipinfo.io

# Proxy string format for tools that want host:port:login:password
#   us-east.isp.catproxies.com:1337:cpx8f31a2-203.0.113.24:Rk4pX9wq

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

#### Targeting

There is nothing to append. The city, carrier and UDP choice are fixed when you buy, and each line is one fixed IP.

#### Renewing

Yes. POST /purchase/extend with just the orderId and paymentMethod, no packageId: it renews the same plan (same length, same IPs) at its per-IP price, plus the carrier fee per IP again when the plan has a carrier. To renew for another length, for example a 1-day plan for 1 month, add length ("1_day", "7_days", "1_month", "2_months" or "3_months"); options in GET /purchase/isp-extend-price lists the lengths and their prices. IP replacements are not renewed. Check the cost first with GET /purchase/isp-extend-price?orderId=... One renewal per plan at a time. If only some IPs could be renewed, the renewal still succeeds and our team completes the rest; do not renew again.

#### Good to know

- **Tiers:** standard is Standard. premium is Clean Score: the IPs are checked right before delivery and have a fraud score of 0 when you get them. clean_score on GET /generator/isp is true on these plans.
- **Do not check IPs through the proxy:** We check every IP before delivery. Please do not visit IP check or fraud score sites (IPQS and similar) through the proxy: those sites log every visit, and it can raise the IP's fraud score. To check an IP, look it up from another device or a connection that is not using the proxy.
- **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.
- **location:** Send the city exactly as the catalog returns it, as "Country - City". The catalog counts are at most a minute old, so the purchase checks stock again and refuses cleanly (nothing charged) when a city or carrier ran out.
- **Carrier (asn):** Optional. Take it from GET /custom-isp/carriers for the same city, tier and udp. It adds carrierFee per IP. Without it you get IPs from any carrier in that city.
- **IP replacements:** 0 to 5, bought with the plan at its replacementPrice each. POST /custom-isp/orders/:id/replace with { ip } swaps that IP for a new one in the same city; the city and UDP support stay, but for now the carrier (ASN) can be different. Renewing keeps the same IPs. HTTP 202 with { checking: true } means the swap is still being confirmed: do not send it again. Replacements belong to that plan and are not added again when you renew. 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.
- **Personal price and discounts:** pricePerIp in the catalog already includes any launch discount and your personal price. Coupons are refused when either applies (customRate true, or promoPct above 0).
- **Authentication:** Username and password only. There is no IP whitelist and no password reset on this product.
- **Just after buying:** The reply can say settingUp while the IPs are placed, and GET /generator/isp can answer HTTP 202 { pending: true }. Wait a few seconds and ask again. Do not buy again.
- **Stock problems and errors:** Sometimes a city or carrier runs out, or delivery fails. Stock is checked before you pay, so most of these are refused with nothing charged. If it fails after you paid, the plan shows as being set up and we retry it automatically: after about 1, 3, 6 and 11 minutes, then every 10 minutes for up to 6 hours. If it is still not delivered, or the result is unclear, our team checks whether the IPs were delivered and finishes the plan or refunds it within 24 hours. Do not buy again in the meantime.

### Dedicated ISP (`IspP`)

*Private static IPs, nobody else on them.*

#### What it is

Sold per IP, per month, and the IPs are yours alone rather than from a shared pool. There is no country to choose, and the term is a fixed 30 days. The quantity is not free-form: it has to match one of the sizes offered for the plan.

#### Buying

```bash
# Dedicated ISP takes the quantity only,
# and that quantity must be one of the sizes the plan offers (see /packages).
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "packageId": "PACKAGE_ID",
    "paymentMethod": "Balance",
    "ispData": { "quantity": 5 }
  }'
```

#### Proxy format and connecting

GET /generator/isp?orderId=... returns one line per IP with its own host, port, login and password.

| Field | Meaning |
| --- | --- |
| host | the IP address of that line |
| port_http | HTTP and HTTPS port for that line |
| port_socks | SOCKS5 port for that line, and UDP rides on it too. It is NOT the same number as port_http on this product, so always read this field. Connecting SOCKS5 to the HTTP port fails outright. |
| login / password | credentials for that line only |

**Example**

```bash
# From GET /generator/isp?orderId=...
#   { "host": "203.0.113.24", "port_http": 61234, "port_socks": 62234,
#     "login": "ded_4f21ba", "password": "Kq7ZmTx2" }

# HTTP and HTTPS on port_http
curl -x 203.0.113.24:61234 -U "ded_4f21ba:Kq7ZmTx2" https://ipinfo.io

# SOCKS5, and UDP over SOCKS5, on port_socks. Same host, DIFFERENT port.
curl -x socks5h://203.0.113.24:62234 -U "ded_4f21ba:Kq7ZmTx2" https://ipinfo.io
```

#### Targeting

There is nothing to append. Each IP is a fixed address and the product is pool based, so there is no country flag either.

#### Renewing

Yes. POST /purchase/extend with just the orderId and paymentMethod: it renews the same number of IPs for another 30 days. Check the cost first with GET /purchase/isp-extend-price?orderId=...

#### Good to know

- **Username and password only:** There is no IP-authentication option on this product.
- **IP locations:** Locations are strictly based on the ipinfo.io database. Other IP databases can show a different location for the same IP.
- **stale:** stale: true on GET /generator/isp means the list is our last known good copy. The lines still work; call again shortly for a fresh list.
- **Just after buying:** The lines are normally ready immediately. If the list comes back empty on the first call, give it a few seconds and ask again.
- **Allowed quantities:** Only the sizes listed for the plan in GET /packages?proxyType=IspP are accepted. Anything else is refused with 'Please choose one of the available quantity options for this plan.'
- **Term:** A fixed 30 days per purchase, whatever quantity you buy.

### Datacenter (`DatacenterP`)

*A block of dedicated IPs plus a shared bandwidth pool.*

#### What it is

One plan gives you a list of dedicated IPs and a shared GB pool for a fixed number of days, for example 100 IPs with 250 GB for 30 days. At purchase you decide how those IPs split across US, CA, DE, GB and NL. Every IP is its own proxy and they all share one username and password.

#### Buying

```bash
# country_proxies must add up to EXACTLY the package's IP count.
# Check live stock first with GET /packages/datacenterp-countries.
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "packageId": "PACKAGE_ID",
    "paymentMethod": "Balance",
    "datacenterPData": {
      "country_proxies": { "US": 60, "DE": 20, "GB": 20 },
      "high_concurrency": false,
      "high_priority": false,
      "whitelisted_ips": false
    }
  }'
```

#### Proxy format and connecting

GET /orders/:id returns proxy.username and proxy.password (shared by every IP) plus ip_list, grouped by country and then city. Flatten cities[].ips to get every address. The list is refreshed on each call. There are two ways to connect: STATIC, where you connect to one of your own IPs and always exit from it, and ROTATING, where you connect to a country gateway below, add -ttl-0 to the username and get a new IP from your plan on every new connection. The same credentials work on every IP and every gateway.

| Gateway | HTTP port | SOCKS5 port |
| --- | --- | --- |
| dcww.catproxies.com (worldwide rotating, only on plans with all five countries) | 1338 | 1339 |
| dcus.catproxies.com (United States rotating) | 1338 | 1339 |
| dcca.catproxies.com (Canada rotating) | 1338 | 1339 |
| dcde.catproxies.com (Germany rotating) | 1338 | 1339 |
| dcgb.catproxies.com (United Kingdom rotating) | 1338 | 1339 |
| dcnl.catproxies.com (Netherlands rotating) | 1338 | 1339 |
| Any IP from your ip_list (static) | 1338 | 1339 |

**Example**

```bash
# From GET /orders/:id
#   username: dcuser482913   password: dcpass91827
#   ip_list -> 203.0.113.24, 203.0.113.57, ...
#   rotating.endpoints -> { "US": "dcus.catproxies.com", "DE": "dcde.catproxies.com" }

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

# ROTATING, United States: a new US IP from your plan on every new connection
curl -x dcus.catproxies.com:1338 -U "dcuser482913-ttl-0:dcpass91827" https://ipinfo.io

# ROTATING, Germany
curl -x dcde.catproxies.com:1338 -U "dcuser482913-ttl-0:dcpass91827" https://ipinfo.io

# ROTATING, worldwide (only on a plan that owns IPs in ALL FIVE countries)
curl -x dcww.catproxies.com:1338 -U "dcuser482913-ttl-0:dcpass91827" https://ipinfo.io

# SOCKS5 on port 1339, static and rotating
curl -x socks5h://203.0.113.24:1339 -U "dcuser482913:dcpass91827" https://ipinfo.io
curl -x socks5h://dcus.catproxies.com:1339 -U "dcuser482913-ttl-0:dcpass91827" https://ipinfo.io

# Proxy string format for tools that want host:port:user:pass
#   static:    203.0.113.24:1338:dcuser482913:dcpass91827
#   rotating:  dcus.catproxies.com:1338:dcuser482913-ttl-0:dcpass91827
```

#### Targeting, appended to the username

The country is decided by the address you connect to: one of your own IPs for static, or that country's gateway for rotating. The only flag is the one that turns a line from static into rotating.

| Target | Flag | Value format |
| --- | --- | --- |
| Rotating | `-ttl-0` | a new IP on every new connection, from your own IPs in that country |

- Without the flag you get that exact IP every time, which is the default.
- Only use a gateway for a country your plan has IPs in. A gateway for a country you did not buy answers 407.
- proxy.rotating.endpoints on GET /orders/:id gives the exact rotating host for each country of your plan. It is usually the gateway in the table above. It gives you a plain IP instead when your plan owns one of the gateway addresses, or in the US when all your US IPs sit behind just one of the two US addresses. Reading it from the API is the safest option.
- Do not use one of your own IPs as the rotating host. If an IP is whitelisted, a request through one of your own IPs is logged in by IP, the flag is ignored and you always get that IP.
- proxy.rotating.bound_ip_conflicts lists the countries where your plan owns the only gateway address. Rotating there keeps giving the same IP while an IP is whitelisted, so remove the whitelisted IP if you need rotating in that country. Static is not affected.
- Rotation happens per connection, not per request. If your client keeps the connection alive it will stay on one IP, so open a fresh connection per request (or send a Connection: close header) when you want a new IP each time.

#### Renewing

Yes. Check the price first with GET /orders/:id/extend-options, which lists the packages that qualify and the exact total for each. Then POST /purchase/extend with the orderId and the packageId of a Datacenter package with the same number of IPs and the same bandwidth as your plan; only the duration may differ. 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. Add-ons you bought with the plan (high_concurrency, high_priority, extra whitelist slots) are charged again at +25% each. Read GET /orders/:id afterwards for the new end date. See Renewing a Datacenter or IPv6 plan below.

**Renewing a Datacenter or IPv6 plan**

Three calls: check the price, renew, then read the plan again for the new end date. The price check charges nothing, so call it as often as you like.

```bash
# 1. Check the price. Charges nothing.
#    renewable must be true. Pick one of options[] and note its package_id
#    and total, and show note to whoever is paying.
curl "https://catproxies.com/api/v1/user/orders/ORDER_ID/extend-options" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Renew, paid from your balance (it must cover options[].total).
curl -X POST "https://catproxies.com/api/v1/user/purchase/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "ORDER_ID", "packageId": "PACKAGE_ID_FROM_STEP_1", "paymentMethod": "Balance" }'
# -> payload.ExtendOrderId, payload.extendOrder.amount (what was charged)

# 3. Read the plan again: proxy.expired_at is the new end date,
#    proxy.bandwidth_left the data you now have.
curl "https://catproxies.com/api/v1/user/orders/ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

- Renew before the end date. An expired plan cannot be renewed; buy a new one instead.
- Datacenter: 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. Only packages with the same number of IPs and the same bandwidth qualify, and add-ons are charged again at +25% each.
- 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 proxy.expired_at (and on an IPv6 bandwidth plan raised the data). 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 we need to confirm it with a quick manual check..." means you WERE charged and our team finishes it within 24 hours or refunds it: do not retry.
- One renewal per plan at a time: a second request while one is running gets a 409 and is not charged. Renewals share the 3-per-minute limit with purchases and password resets.
- Related: POST /orders/:id/reset-password rotates the password on both products, and GET /orders/:id/stats shows how the plan is being used.

#### Usage stats

GET /orders/:id/stats gives traffic and requests over time, a log of recent connections and your top sites. See Usage stats below.

#### Good to know

- **Where these hosts come from:** These gateways are fixed, and the same hosts are on the plan page in your dashboard. If we ever move a gateway the hostname stays the same, so build your proxy strings from the names above rather than resolving one to an IP and hard-coding that.
- **United States gateway:** Your US IPs sit behind two addresses, each covering a part of your IPs, and dcus.catproxies.com holds all of them (Canada and Germany work the same way with dcca and dcde). Your machine picks one of them and keeps it for about a minute before it can switch, so within one minute you rotate through part of your IPs and over a few minutes through all of them.
- **Worldwide gateway:** dcww.catproxies.com only works on a plan that owns IPs in all five countries (US, CA, DE, GB and NL). On a smaller plan it answers 407 whenever it lands on a country you did not buy, so use the per-country gateways instead.
- **country_proxies:** Must total exactly the package's IP count. Allowed countries: US, CA, DE, GB and NL. Over-allocating a country fails the purchase, so read GET /packages/datacenterp-countries first.
- **Add-ons:** high_concurrency (more parallel connections), high_priority (priority routing) and whitelisted_ips (3 whitelist slots instead of 1). Each one adds 25% to the price, so all three is +75%. All four keys must be present in datacenterPData.
- **Bandwidth:** One shared GB pool across every IP in the plan. Track it with proxy.bandwidth_left on GET /orders/:id.
- **Errors from the proxy:** 407 means wrong credentials or an address that is not whitelisted. 429 means you hit the plan's concurrent-connection limit, so lower your parallelism or buy with high_concurrency.
- **Rotate the password:** POST /orders/:id/reset-password issues one new password shared by every IP. Swap it into all of your proxy strings.

### IPv6 (`Ipv6p`)

*The cheapest way to make a very large number of requests.*

#### What it is

Rotates through a very large pool of IPv6 addresses, with a fresh exit on every request by default. Two flavours in the catalogue: bandwidth plans with a GB pool and no speed cap, and unlimited plans with no GB cap and a speed cap in Mbps. Buying needs nothing but the packageId.

#### Buying

```bash
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID", "paymentMethod": "Balance" }'
```

#### Proxy format and connecting

GET /orders/:id gives you proxy.username and proxy.password. The exit country is decided by which gateway you connect to, and the same credentials work on all of them.

| Gateway | HTTP port | SOCKS5 port |
| --- | --- | --- |
| ipv6-ww.catproxies.com (worldwide) | 1338 | 1339 |
| ipv6-us.catproxies.com | 1338 | 1339 |
| ipv6-de.catproxies.com | 1338 | 1339 |
| ipv6-gb.catproxies.com | 1338 | 1339 |
| ipv6-nl.catproxies.com | 1338 | 1339 |

**Example**

```bash
# From GET /orders/:id
#   username: ipv6user291   password: v6pass817

# Rotating, a fresh IPv6 address on every request (US gateway)
curl -x ipv6-us.catproxies.com:1338 -U "ipv6user291:v6pass817" https://api6.ipify.org

# Sticky, hold the same exit for 60 minutes (ttl is in SECONDS, so 3600)
curl -x ipv6-de.catproxies.com:1338 \
  -U "ipv6user291-session-483920-ttl-3600:v6pass817" \
  https://api6.ipify.org

# SOCKS5
curl --socks5 ipv6-us.catproxies.com:1339 -U "ipv6user291:v6pass817" https://api6.ipify.org
```

#### Targeting, appended to the username

The country comes from the gateway you pick. The only flags are the ones that hold an IP still.

| Target | Flag | Value format |
| --- | --- | --- |
| Sticky session | `-session-483920-ttl-3600` | session id is 6 digits; ttl is in SECONDS, up to 86400 (24h) |

- Without the session flags you get a new IPv6 address on every request.
- The session id is a 6-digit number, 000000 to 999999. Pick your own and reuse it to keep the same exit, or generate a fresh one per line.
- ttl is in seconds, not minutes: 1800 is 30 minutes and 3600 is one hour. The maximum is 86400, which is 24 hours.
- Use different session ids to hold several sticky IPs at the same time.

#### Renewing

Yes. Check the price first with GET /orders/:id/extend-options, which lists the packages that qualify and the exact total for each. Then POST /purchase/extend with the orderId and the packageId of a plan of the same kind. An unlimited plan renews with the unlimited package of the same speed and the same length (a 30-day plan renews for 30 days), added on top of your current end date. A bandwidth plan renews with any IPv6 bandwidth package that keeps it at or under 5000 GB in total (the most an IPv6 plan can hold): its GB are added to what you have left, and the end date moves forward by your plan's own term (the number of days it was bought with). Read GET /orders/:id afterwards for the new end date and bandwidth. See Renewing a Datacenter or IPv6 plan below.

**Renewing a Datacenter or IPv6 plan**

Three calls: check the price, renew, then read the plan again for the new end date. The price check charges nothing, so call it as often as you like.

```bash
# 1. Check the price. Charges nothing.
#    renewable must be true. Pick one of options[] and note its package_id
#    and total, and show note to whoever is paying.
curl "https://catproxies.com/api/v1/user/orders/ORDER_ID/extend-options" \
  -H "Authorization: Bearer cp_your_api_key_here"

# 2. Renew, paid from your balance (it must cover options[].total).
curl -X POST "https://catproxies.com/api/v1/user/purchase/extend" \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "ORDER_ID", "packageId": "PACKAGE_ID_FROM_STEP_1", "paymentMethod": "Balance" }'
# -> payload.ExtendOrderId, payload.extendOrder.amount (what was charged)

# 3. Read the plan again: proxy.expired_at is the new end date,
#    proxy.bandwidth_left the data you now have.
curl "https://catproxies.com/api/v1/user/orders/ORDER_ID" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

- Renew before the end date. An expired plan cannot be renewed; buy a new one instead.
- IPv6 unlimited plan: the package's days are added on top of your current end date. IPv6 bandwidth plan: the package's GB are added on top of the data you have left, and the end date moves forward by the plan's own term (the number of days it was bought with, which is the package's days when you renew with the plan's own package).
- 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 proxy.expired_at (and on an IPv6 bandwidth plan raised the data). 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 we need to confirm it with a quick manual check..." means you WERE charged and our team finishes it within 24 hours or refunds it: do not retry.
- One renewal per plan at a time: a second request while one is running gets a 409 and is not charged. Renewals share the 3-per-minute limit with purchases and password resets.
- Related: POST /orders/:id/reset-password rotates the password on both products, and GET /orders/:id/stats shows how the plan is being used.

#### Usage stats

GET /orders/:id/stats gives traffic and requests over time, a log of recent connections and your top sites. See Usage stats below.

#### Good to know

- **Where these hosts come from:** These gateways are fixed, and the same list is on the plan page in your dashboard, which is always current. If we ever move a gateway the hostname stays the same, so build your proxy strings from the names above rather than resolving one to an IP and hard-coding that.
- **Your target site must support IPv6:** Exits are IPv6 only, so the destination needs an AAAA DNS record. Google, YouTube, Facebook, Instagram and TikTok all do; many smaller sites do not and will simply be unreachable. Test your target first, then confirm the proxy end to end against https://api6.ipify.org. A site with no IPv6 usually shows up as a 503 from the proxy.
- **No IP whitelist:** IPv6 plans are username and password only. There is no whitelist on this product.
- **Bandwidth:** On bandwidth plans, proxy.bandwidth_left on GET /orders/:id is what is left. On unlimited plans there is no GB figure; the plan is capped by speed instead.
- **Rotate the password:** POST /orders/:id/reset-password issues a new password. The session flags live on the username, so only the password half of your proxy strings changes.

### Rotating Mobile (`RotatingMobile`)

*Real mobile carrier IPs, sold by the gigabyte.*

#### What it is

Sold by bandwidth and bought with nothing but the packageId. One set of credentials covers every location; you choose the location with flags on the username. A fresh IP on every request by default.

#### Buying

```bash
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "packageId": "PACKAGE_ID", "paymentMethod": "Balance" }'
```

#### Proxy format and connecting

GET /orders/:id gives you proxy.username and proxy.password. The host is one of the three fixed regional gateways below: pick the one nearest your target country, since crossing continents costs real latency. The same credentials work on all of them, and one port serves every location. Ignore proxy.server_ip on this product; it is not set.

| Gateway | Port | Protocol |
| --- | --- | --- |
| mobile-us.catproxies.com (Americas) | 5000 | HTTP and HTTPS |
| mobile-eu.catproxies.com (Europe) | 5000 | HTTP and HTTPS |
| mobile-asia.catproxies.com (Asia and Oceania) | 5000 | HTTP and HTTPS |

**Example**

```bash
# From GET /orders/:id
#   username: mob8814720   password: r4Xq9tLp
# Host: pick the gateway nearest your target from the table above

# Untargeted rotation
curl -x mobile-us.catproxies.com:5000 -U "mob8814720:r4Xq9tLp" https://ipinfo.io

# United States, a specific carrier, pinned at country level
curl -x mobile-us.catproxies.com:5000 \
  -U "mob8814720-country-US-isp-at&t+wireless:r4Xq9tLp" \
  https://ipinfo.io

# Hold one IP for 30 minutes
curl -x mobile-us.catproxies.com:5000 \
  -U "mob8814720-country-US-sid-ab12-ttl-30m:r4Xq9tLp" \
  https://ipinfo.io
```

#### Targeting, appended to the username

Leave the password alone and append flags to the username. Every flag is optional; with none of them you get untargeted rotation.

| Target | Flag | Value format |
| --- | --- | --- |
| Country | `-country-US` | ISO-2 code |
| Region or state | `-region-california` | full name in lowercase, from /generator/mobile-locations/regions |
| City | `-city-los+angeles` | from /generator/mobile-locations/cities. Spaces are a plus sign. |
| Carrier | `-isp-at&t+wireless` | from /generator/mobile-locations/isps |
| Sticky session | `-sid-ab12-ttl-30m` | any id, plus one of 1m 5m 10m 30m 60m 120m 180m 240m |

- Write spaces as a plus sign, never a hyphen: los+angeles, new+york, at&t+wireless. Regions are full names such as california, not two-letter codes.
- An unknown or malformed value fails outright rather than quietly falling back to untargeted rotation. If a targeted request errors, check the code against the targeting endpoints first.
- Carrier coverage varies by city, so a valid city and carrier pair can still fail if that carrier has no live IP there right now. For dependable carrier targeting, pin the carrier at country level and leave the city off. City targeting is approximate to the metro area, not pinpoint.
- Untargeted rotation is not evenly spread across carriers. If you need a particular mix, ask for each carrier explicitly.

#### Renewing

Yes. POST /purchase/extend with the orderId and the packageId of the size you want.

#### Good to know

- **Where these hosts come from:** These gateways are fixed, and the same list is on the plan page in your dashboard, which is always current. If we ever move a gateway the hostname stays the same, so build your proxy strings from the names above rather than resolving one to an IP and hard-coding that.
- **Bandwidth:** proxy.bandwidth_left on GET /orders/:id is what you have left, in GB.
- **Reset the login:** POST /orders/:id/reset-password gives the plan a new login and moves your remaining data onto it. The username AND the password both change, so rebuild your proxy strings from the username and password in the response. The old login stops working straight away. Once per hour per plan.
- **Cache the targeting lists:** The four /generator/mobile-locations/* endpoints are cached on our side for an hour and the lists change rarely, so fetch them once at startup rather than per request. They still count against your hourly budget.
- **Sticky windows:** ttl accepts only the listed values, up to 240m (4 hours). Anything else is rejected.

### Usage stats

Datacenter and IPv6 plans 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 /orders/:id/stats, and the view parameter picks which of the three you get. It never changes the plan.

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

# The last week, one row per day
curl "https://catproxies.com/api/v1/user/orders/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/user/orders/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/user/orders/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 proxy.bandwidth_left on GET /orders/: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 /orders/: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/user`. Responses are JSON shaped as `{ type, message, payload, pagination? }`. Check `type` as well as the HTTP status, because some handled failures answer 200 with `type: "error"`.

### Account

#### `GET /account`

Everything a client normally needs about the account itself: the balance orders are paid from, how many plans you have, and your rate limit.

Request:

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

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "email": "you@example.com",
    "displayName": "Alex",
    "country": "Germany",
    "balance": 42.5,
    "currency": "USD",
    "pricing": "retail",
    "totalOrders": 12,
    "activeOrders": 3,
    "rateLimits": { "requestsPerHour": 1000 }
  }
}
```

#### `GET /balance`

Your deposit and spend history, newest first. Each row is one movement. Use /account for the current balance figure itself; this endpoint returns the ledger, not a running total.

Request:

```bash
curl https://catproxies.com/api/v1/user/balance \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "balances": [
      {
        "id": "b41e9d02-7c3a-4f21-9b88-2c5d1e6a0f34",
        "type": "Expense",
        "balance": 2.5,
        "currency": "USD",
        "status": "CONFIRMED",
        "paymentMethod": "Balance",
        "proxyType": "v2resi",
        "orderId": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
        "extendOrderId": null,
        "invoice_number": null,
        "createdAt": "2026-09-04T10:12:00.000Z",
        "order": {
          "id": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
          "package_title": "Standard Resi 1GB V2",
          "package_proxyType": "v2resi",
          "amount": 2.5
        }
      }
    ]
  },
  "pagination": { "page": 1, "pageSize": 10, "Total": 34, "Pages": 4 }
}
```

### Catalogue

#### `GET /packages`

The same catalogue, and the same retail prices, as the Purchase Plan page in your dashboard. Filter by product with `proxyType`. A package `id` from here is what you send to POST /purchase.

Custom ISP is the exception. With `proxyType=Isp`, the list starts with a marker entry `{ "id": "customisp", "title": "Custom ISP", "customIsp": true }` while Custom ISP is on sale. It is not a package and cannot be bought by that id. Custom ISP plans, prices and cities come from GET /custom-isp/catalog.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `proxyType` | string | no | Product to list: "v2resi", "resix", "Isp", "IspP", "DatacenterP", "Ipv6p" or "RotatingMobile". |
| `page` | number | no | Page number. Default 1. |
| `pageSize` | number | no | Results per page. Default 10. |
| `search` | string | no | Free-text filter on the plan title. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/packages?proxyType=v2resi&pageSize=50" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "packages": [
      {
        "id": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f",
        "title": "Standard Resi 1GB V2",
        "description": "Standard Resi 1GB",
        "price": 2.5,
        "bandwidth": 1,
        "days": 90,
        "ips": null,
        "speed": 0,
        "proxyType": "v2resi",
        "hidden": false
      }
    ]
  },
  "pagination": { "page": 1, "pageSize": 50, "Total": 6, "Pages": 1 }
}
```

#### `GET /packages/:id`

The full record for a single package: price, bandwidth or IP count, duration and product type.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Package id from /packages. |

Request:

```bash
curl https://catproxies.com/api/v1/user/packages/cdd0c81b-43fd-4bef-9709-2173a5ad3c5f \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "package": {
      "id": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f",
      "title": "Standard Resi 1GB V2",
      "price": 2.5,
      "bandwidth": 1,
      "days": 90,
      "proxyType": "v2resi"
    }
  }
}
```

#### `GET /packages/datacenterp-countries`

How many Datacenter IPs are free right now in each country. Check this before you allocate, because asking for more than a country has will fail the purchase.

Request:

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

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": [
    { "name": "United States", "code": "US", "available": 4180 },
    { "name": "Germany", "code": "DE", "available": 970 }
  ]
}
```

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

Everything you need to build a Custom ISP order. Custom ISP is static ISP proxies where you pick the tier, the plan length, the city and, if you want, the carrier.

plans lists one package per tier and length. Its packageId is what you send to POST /purchase. pricePerIp is the price you pay for one IP for that length. It already includes any launch discount and your personal price. 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 two tiers: standard, and premium, which is shown as Clean Score on the website. Clean Score IPs are checked right before delivery and have a fraud score of 0 when you get them. 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, so a city can run out between this call and your purchase. The purchase checks stock again and refuses cleanly if it did.

When Custom ISP is not on sale the payload is just `{ "enabled": false }`. When the tier you asked for has no plans, countries is empty. Limited to 40 requests per minute per account.

**Query parameters**

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

Request:

```bash
curl "https://catproxies.com/api/v1/user/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_day": { "packageId": "a1b2c3d4-0001-4a5b-8c9d-0e1f2a3b4c5d", "pricePerIp": 0.6, "listPricePerIp": 0.6, "promoPct": 0, "customRate": false, "days": 1, "minQty": 1, "maxQty": 100, "replacementPrice": 1.1 },
        "1_month": { "packageId": "a1b2c3d4-0003-4a5b-8c9d-0e1f2a3b4c5d", "pricePerIp": 3.6, "listPricePerIp": 4, "promoPct": 10, "customRate": false, "days": 30, "minQty": 1, "maxQty": 100, "replacementPrice": 1.1 }
      },
      "premium": {
        "1_month": { "packageId": "a1b2c3d4-0103-4a5b-8c9d-0e1f2a3b4c5d", "pricePerIp": 5, "listPricePerIp": 5, "promoPct": 0, "customRate": false, "days": 30, "minQty": 1, "maxQty": 100, "replacementPrice": 1.1 }
      }
    },
    "carrierFee": 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 }
        ]
      }
    ]
  }
}
```

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `enabled` | boolean | false when Custom ISP is not on sale right now. Nothing else is sent then. |
| `tiers` | string[] | Tiers that have at least one plan: "standard" and/or "premium" (Clean Score). |
| `plans[tier][length]` | object | One plan per tier and length. Lengths: "1_day", "7_days", "1_month", "2_months", "3_months". Only lengths on sale are listed. |
| `plans[tier][length].packageId` | string | Send this as packageId to POST /purchase. |
| `plans[tier][length].pricePerIp` | number | What one IP costs you for this length, before VAT. Includes any launch discount and your personal price. |
| `plans[tier][length].listPricePerIp` | number | The normal price per IP, before any discount. |
| `plans[tier][length].promoPct` | number | Launch discount in percent, 0 when none. Coupons are refused while it is above 0. |
| `plans[tier][length].customRate` | boolean | true when your personal price applies. Coupons are refused then too. |
| `plans[tier][length].days` | number | Plan length in days. |
| `plans[tier][length].minQty, maxQty` | number | How many IPs one order can have. |
| `plans[tier][length].replacementPrice` | number \| null | Price of one IP replacement bought with this plan. null when replacements are not sold on it. |
| `carrierFee` | number | Extra price per IP when you pick a carrier (ispData.asn). |
| `maxReplacements` | number | Most IP replacements one order can be bought with (5). |
| `countries[].total` | number | IPs in stock in that country for this tier and UDP choice. |
| `countries[].cities[].location` | string | The city as "Country - City". Send it exactly like this as ispData.location and to GET /custom-isp/carriers. |
| `countries[].cities[].count` | number | IPs in stock in that city. |

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | Live stock is unavailable right now. Please try again in a minute. | We could not read the stock just now. Nothing to fix on your side; try again shortly. |
| 429 | Too many requests. Please wait a moment. | More than 40 calls in a minute. Cache the result for a minute instead of polling. |

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

The carriers (ASNs) with IPs in stock in one city, for one tier and UDP choice. Picking a carrier is optional. If you do, send its asn as ispData.asn on POST /purchase and every IP costs carrierFee more.

location must be a city exactly as GET /custom-isp/catalog returns it. Carriers are listed best first: tier 1 is a top consumer carrier, then ISP, backbone network and hosting network. The list is 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, for example "United States - New York City", or a country name (always answers an empty list: with any city the carrier is always any). |
| `tier` | string | no | "standard" (default) or "premium". Use the same tier you will buy. |
| `udp` | string | no | "1" to count only IPs with UDP support. Use the same choice you will buy. |

Request:

```bash
curl -G "https://catproxies.com/api/v1/user/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": {
    "carrierFee": 0.35,
    "tierLabels": { "1": "Top carrier", "2": "ISP", "3": "Backbone network", "4": "Hosting network" },
    "carriers": [
      { "asn": 7922, "name": "Comcast Cable Communications", "stock": 96, "tier": 1 },
      { "asn": 701, "name": "Verizon Business", "stock": 41, "tier": 1 }
    ]
  }
}
```

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `carrierFee` | number | Extra price per IP when you pick one of these carriers. |
| `tierLabels` | object | Names for the carrier tiers 1 to 4. |
| `carriers[].asn` | number | Send this as ispData.asn on POST /purchase. |
| `carriers[].name` | string | The carrier's name. |
| `carriers[].stock` | number | IPs on that carrier in stock in this city. |
| `carriers[].tier` | number | 1 is best. See tierLabels. |

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | This city is not available. | location is not a city from the catalog, or it has no IPs in stock for this tier and UDP choice. |
| 400 | Carrier stock is unavailable right now. Please try again in a minute. | We could not read the stock just now. Try again shortly. |
| 400 | validation error | location is missing or longer than 120 characters. |
| 400 | This product is not available right now. | Custom ISP is not on sale at the moment. |
| 429 | Too many requests. Please wait a moment. | More than 40 calls in a minute. |

### Buying and renewing

#### `POST /purchase`

Creates the order, charges your account balance and sets the plan up. The response contains the new order; read its credentials with GET /orders/:id. `paymentMethod` must be "Balance": an API order cannot open a card checkout, so top up on the website first if the balance is short.

If the plan could not be set up on the spot, the response can come back with `payload.settingUp: true` and `order.status: "PENDING"`. You have been charged and the plan is being set up automatically, which usually takes a few minutes. Do not buy again: keep reading GET /orders/:id until it shows the plan's credentials. Plans still being set up are also listed in `payload.settingUp` on GET /orders.

Custom ISP: take packageId from GET /custom-isp/catalog (plans[tier][length].packageId) and send your picks in ispData: quantity and location are required, asn, udp and replacements are optional. The price is worked out on our side: price per IP x IPs, plus the carrier fee per IP x IPs when you set asn, plus replacements x the plan's replacement price. VAT is added on top as for any purchase. Live stock is checked before you are charged. A Custom ISP plan can also answer with settingUp while its IPs are placed: it retries by itself after 2, 7 and 12 minutes, so read GET /generator/isp a little later for the lines. If it still gets no IP, the order is cancelled and the price goes back to your balance automatically. If your last 3 orders for one exact choice all got no IP, that choice is refused for a while with HTTP 409 and payload.code CUSTOM_ISP_CHOICE_BLOCKED (nothing charged).

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `packageId` | string | yes | The package to buy, from /packages. |
| `paymentMethod` | string | yes | Always "Balance" for API orders. |
| `ispData` | object | no | ISP products only. Dedicated ISP: { quantity }. Custom ISP: { quantity, location, asn?, udp?, replacements?, price? }, see the ispData rows below. |
| `ispData.quantity` | integer | no | Custom ISP: number of IPs, within the plan's minQty and maxQty from the catalog. |
| `ispData.location` | string | no | Custom ISP: the city, exactly as GET /custom-isp/catalog returns it ("Country - City"), for example "United States - New York City". Or send just the country (countries[].country, for example "United States") to get IPs from any city in it: its stock is countries[].total. A country cannot be combined with ispData.asn. |
| `ispData.asn` | number | no | Custom ISP, optional: a carrier from GET /custom-isp/carriers for that city. Adds the carrier fee per IP. Leave it out for any carrier. |
| `ispData.udp` | boolean | no | Custom ISP, optional: true gives 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, optional: IP replacements bought with the plan, 0 to 5, each at the plan's replacementPrice. Default 0. They are used with POST /custom-isp/orders/:id/replace and are 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. |
| `ispData.price` | number | no | Custom ISP, optional: the total before VAT you expect to pay. If you send it, it must match our total within 0.02 or the order is refused. Our own total is always what is charged. |
| `datacenterPData` | object | no | Datacenter only. { country_proxies: { US: 60, DE: 20 }, high_concurrency, high_priority, whitelisted_ips }. All four keys are required. |

Request:

```bash
# Any bandwidth product: just the packageId
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "packageId": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f",
    "paymentMethod": "Balance"
  }'

# Custom ISP: 5 IPs in New York City on one carrier, with 2 IP replacements
curl -X POST https://catproxies.com/api/v1/user/purchase \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "packageId": "a1b2c3d4-0003-4a5b-8c9d-0e1f2a3b4c5d",
    "paymentMethod": "Balance",
    "ispData": {
      "quantity": 5,
      "location": "United States - New York City",
      "asn": 7922,
      "udp": false,
      "replacements": 2
    }
  }'
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "PaymentMethod": "Balance",
    "PaymentPayload": { "hosted_url": "https://catproxies.com/dashboard/my-plans/active-plans" },
    "OrderId": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
    "order": {
      "id": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
      "status": "CONFIRMED",
      "package_title": "Standard Resi 1GB V2",
      "package_proxyType": "v2resi",
      "amount": 2.5,
      "expire": "2026-12-03T12:00:00.000Z"
    }
  }
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | ISP Region and proxy amount are required | Custom ISP: ispData.quantity or ispData.location is missing. |
| 400 | Quantity must be between 1 and 100. | Custom ISP: the quantity is outside the plan's minQty and maxQty. |
| 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 or below 0. |
| 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 and payload.retryAfterMinutes says when this exact choice opens again. Nothing was charged. |
| 400 | Invalid ISP price | Custom ISP: the price you sent differs from our total by more than 0.02. Read the catalog again; prices or your discount may have changed. |
| 400 | Insufficient balance | Top up and try again. Nothing was charged. |
| 400 | ISP packages are currently unavailable. | Custom ISP is switched off right now. Nothing was charged. |
| 409 | This product is not available at the moment. Please try again later. | New purchases of this product or plan are paused. Nothing was charged. |

#### `GET /orders/:id/extend-options`

A price check before you renew a Datacenter or IPv6 plan. It lists every package this plan can be renewed with, and exactly what POST /purchase/extend would take from your balance for each one. It charges nothing and changes nothing.

Which packages are listed:
• Datacenter: packages with the same number of IPs and the same bandwidth as the plan. Only the duration differs. The plan's own package is always included.
• IPv6 unlimited plan: the unlimited package with the same speed and the same length as the plan.
• IPv6 bandwidth plan: every IPv6 bandwidth package that keeps the plan at or under 5000 GB in total (the most an IPv6 plan can hold).
The plan's current package comes first (is_current_package: true), then the rest, shortest first.

note says what the renewal does to the plan's data and days, in the same words as the renew window on the website. On a Datacenter plan with a data cap it reads: "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.) Show it to whoever is paying.

price is before VAT: the package price, times the add-on multiplier on Datacenter (+25% for each add-on the plan was bought with). total is what the renewal takes from your balance, with VAT and any balance fee included. Coupons are not applied here, so a valid coupon on the renewal lowers the real charge.

When renewable is false, options is empty and reason says why, in the same words the renewal itself would use. renewable: true is not a promise: the renewal can still be refused for things this check does not look at, such as your balance, your profile details or account restrictions.

Custom ISP and Dedicated ISP are not covered here and answer 400. Use GET /purchase/isp-extend-price for those.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id of a Datacenter or IPv6 plan. |

Request:

```bash
curl https://catproxies.com/api/v1/user/orders/c5c7c6b5-e488-420c-b436-e90e86031751/extend-options \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// Datacenter plan (100 IPs, 250 GB, bought with one add-on, account VAT 21%)
{
  "type": "response",
  "message": "Success",
  "payload": {
    "order_id": "c5c7c6b5-e488-420c-b436-e90e86031751",
    "product": "Datacenter",
    "renewable": true,
    "reason": null,
    "expires_at": "2026-10-11T10:00:00.000Z",
    "renews_by": "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.",
    "addons": {
      "high_concurrency": true,
      "high_priority": false,
      "extra_whitelist_slots": false,
      "count": 1,
      "price_multiplier": 1.25
    },
    "vat_rate": 0.21,
    "currency": "USD",
    "options": [
      {
        "package_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
        "title": "Datacenter Proxies - 1 Month - 100 IPs - 250GB",
        "is_current_package": true,
        "days": 30,
        "bandwidth_gb": 250,
        "ips": 100,
        "speed": null,
        "adds": "30 days",
        "price": 125,
        "vat": 26.25,
        "fee": 0,
        "total": 151.25
      },
      {
        "package_id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
        "title": "Datacenter Proxies - 3 Months - 100 IPs - 250GB",
        "is_current_package": false,
        "days": 90,
        "bandwidth_gb": 250,
        "ips": 100,
        "speed": null,
        "adds": "90 days",
        "price": 300,
        "vat": 63,
        "fee": 0,
        "total": 363
      }
    ]
  },
  "pagination": {}
}

// IPv6 unlimited plan (100 Mbps, no VAT on this account). There is no addons key on IPv6
{
  "type": "response",
  "message": "Success",
  "payload": {
    "order_id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
    "product": "IPv6",
    "renewable": true,
    "reason": null,
    "expires_at": "2026-10-02T08:30:00.000Z",
    "renews_by": "days",
    "note": "Renewing adds the package's days on top of your current end date.",
    "vat_rate": 0,
    "currency": "USD",
    "options": [
      {
        "package_id": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
        "title": "IPv6 Unlimited 100 Mbps 30 Days",
        "is_current_package": true,
        "days": 30,
        "bandwidth_gb": null,
        "ips": null,
        "speed": 100,
        "adds": "30 days",
        "price": 25,
        "vat": 0,
        "fee": 0,
        "total": 25
      }
    ]
  },
  "pagination": {}
}

// A plan that cannot be renewed
{
  "type": "response",
  "message": "Success",
  "payload": {
    "order_id": "c5c7c6b5-e488-420c-b436-e90e86031751",
    "product": "Datacenter",
    "renewable": false,
    "reason": "Cannot extend an expired proxy",
    "expires_at": "2026-09-01T10:00:00.000Z",
    "renews_by": "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.",
    "addons": { "high_concurrency": false, "high_priority": false, "extra_whitelist_slots": false, "count": 0, "price_multiplier": 1 },
    "vat_rate": null,
    "currency": "USD",
    "options": []
  },
  "pagination": {}
}
```

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `product` | string | "Datacenter" or "IPv6". |
| `renewable` | boolean | false means POST /purchase/extend will refuse this plan right now. reason says why. |
| `reason` | string \| null | Why the plan cannot be renewed, or null. The same message the renewal would give. |
| `expires_at` | string \| null | The plan's current end date (ISO 8601). |
| `renews_by` | string | "days" (Datacenter, IPv6 unlimited plans: the renewal adds time) or "bandwidth" (IPv6 bandwidth plans: the renewal adds GB). |
| `note` | string \| null | What a renewal does to the plan's data and days, in the same words as the renew window on the website. Worth showing to whoever pays. On a Datacenter plan with a data cap it warns that the data resets and unused data does not carry over; null on a Datacenter plan with unlimited bandwidth. Always set on IPv6: on an unlimited plan it says the package's days are added to your end date, on a bandwidth plan that its GB are added to what you have left and the end date moves forward by your plan's own term. |
| `addons` | object | Datacenter only. Which add-ons the plan was bought with (high_concurrency, high_priority, extra_whitelist_slots), how many (count) and the price multiplier they give (1 + 0.25 per add-on). |
| `vat_rate` | number \| null | Your account's VAT rate as a fraction (0.21 = 21%). null when the plan is not renewable. |
| `options[].package_id` | string | Send this as packageId to POST /purchase/extend. |
| `options[].is_current_package` | boolean | true for the package the plan was bought with. |
| `options[].days` | number | Length of that package in days. |
| `options[].bandwidth_gb` | number \| null | GB in that package. null on unlimited IPv6 packages. |
| `options[].ips` | number \| null | Datacenter only: IP count (always the same as the plan's). |
| `options[].speed` | number \| null | IPv6 unlimited only: speed cap in Mbps (always the same as the plan's). |
| `options[].adds` | string | What the renewal adds, in words: "1 day", "30 days" or "50 GB". |
| `options[].price` | number | Price before VAT, add-ons included. |
| `options[].vat` | number | VAT on top of price. |
| `options[].fee` | number | Any fee for paying from balance (0 when none applies). |
| `options[].total` | number | What POST /purchase/extend takes from your balance for this package, before any coupon. |

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 404 | Order not found | No plan with that id on your account. |
| 400 | Renewal options are listed for Datacenter and IPv6 plans. For ISP plans use GET /api/v1/user/purchase/isp-extend-price. | The plan is another product. Other metered products renew with any package from /packages; ISP has its own price check. |
| 500 | Internal server error | Our side failed. Nothing was charged; safe to retry. |

#### `POST /purchase/extend`

Renews a plan you already own, paid from your balance. What you get is always worked out from the package on our side and never from the request, so send the order and, where the product offers a choice of renewal size, the package you want. Renew BEFORE the plan expires: an expired plan cannot be renewed back to life, and on ISP the IPs are released when it lapses, so you get different ones next time.

What a renewal adds depends on the product:
• Standard Residential, Premium Residential, Rotating Mobile: the bandwidth of the package you chose is ADDED to what is left, and the expiry date is pushed out by that package's term (on Standard Residential the end date becomes today plus the package's days).
• Custom ISP: by default the same plan again (same length, same IPs) at its per-IP price, plus the carrier fee per IP again when the plan was bought on a carrier. To renew for another length, for example a 1-day plan for 1 month, send length: "1_day", "7_days", "1_month", "2_months" or "3_months". It is then priced at that length's per-IP price. GET /purchase/isp-extend-price lists the lengths you can pick (options) and the price of each. IP replacements are not renewed. Leave packageId out.
• Dedicated ISP: another full period at its own per-IP price, keeping the same IPs. Leave packageId out.
• Datacenter: packageId is required, and must be a Datacenter package with the same number of IPs and the same bandwidth as the plan. Only the duration may differ, and the plan's own package always qualifies. 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. Add-ons the plan was bought with are charged again at +25% each.
• IPv6: packageId is required. An unlimited (speed) plan renews only with an unlimited IPv6 package of the same speed and the same length as the plan (a 30-day plan renews for 30 days), and those days are added on top of the current end date. A bandwidth plan renews with any IPv6 bandwidth package that keeps it at or under 5000 GB in total (the most an IPv6 plan can hold): its GB are added to what is left, and the end date moves forward by the plan's own term (the number of days it was bought with). The plan's own package qualifies while it is still on sale.

For Datacenter and IPv6, GET /orders/:id/extend-options lists the packages that qualify and the exact total for each. Take packageId from there.

VAT is added on top at your account's rate, exactly as on the website. A coupon, if you send one, comes off before VAT.

The response confirms the charge but does not carry the new end date or bandwidth. Read GET /orders/:id afterwards for those.

Datacenter, IPv6 and Custom ISP: only one renewal per plan can run at a time. A second request while the first is still running gets a 409 and is not charged.

Custom ISP and Dedicated ISP: check the cost first with GET /purchase/isp-extend-price.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The plan to renew. |
| `paymentMethod` | string | yes | Always "Balance" for API renewals. |
| `packageId` | string | no | Which renewal package to buy. Required for Standard Residential, Premium Residential, Rotating Mobile, Datacenter and IPv6. For Datacenter and IPv6, take it from GET /orders/:id/extend-options. Leave it out for Custom ISP and Dedicated ISP, which renew at their own per-IP price. |
| `length` | string | no | Custom ISP only, optional. Renew for this length instead of the plan's own: "1_day", "7_days", "1_month", "2_months" or "3_months". Take it from options[].periodId of GET /purchase/isp-extend-price. Leave it out to renew for the plan's own length. |
| `coupon` | string | no | Optional coupon code. Comes off the price before VAT. An unknown, expired or used-up code is refused and nothing is charged. |
| `type, value, amount` | any | no | Ignored. Older clients send them; the charge and what is added always come from the package. |

Request:

```bash
# Datacenter or IPv6: packageId from GET /orders/:id/extend-options
curl -X POST https://catproxies.com/api/v1/user/purchase/extend \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "c5c7c6b5-e488-420c-b436-e90e86031751",
    "packageId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
    "paymentMethod": "Balance"
  }'

# Residential or Mobile: packageId of the size you want, from /packages
curl -X POST https://catproxies.com/api/v1/user/purchase/extend \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
    "packageId": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f",
    "paymentMethod": "Balance"
  }'

# Custom ISP or Dedicated ISP: no packageId
curl -X POST https://catproxies.com/api/v1/user/purchase/extend \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "8a08caa7-e1f2-487e-aedb-9ebc018ab8f0", "paymentMethod": "Balance" }'

# Custom ISP for another length, e.g. a 1-day plan for 1 month
curl -X POST https://catproxies.com/api/v1/user/purchase/extend \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "8a08caa7-e1f2-487e-aedb-9ebc018ab8f0", "length": "1_month", "paymentMethod": "Balance" }'
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "PaymentMethod": "Balance",
    "PaymentPayload": { "hosted_url": "https://catproxies.com/dashboard/my-plans/active-plans" },
    "ExtendOrderId": "77aa1e08-6c52-4b6f-a0d1-9f3c2e5b8a17",
    "OrderId": "c5c7c6b5-e488-420c-b436-e90e86031751",
    "extendOrder": {
      "id": "77aa1e08-6c52-4b6f-a0d1-9f3c2e5b8a17",
      "orderId": "c5c7c6b5-e488-420c-b436-e90e86031751",
      "status": "CONFIRMED",
      "amount": 151.25
    },
    "paymentUrl": "https://catproxies.com/dashboard/my-plans/active-plans"
  }
}
```

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `ExtendOrderId` | string | Id of this renewal. Keep it as your reference. |
| `OrderId` | string | The plan you renewed. |
| `extendOrder.status` | string | "CONFIRMED": the renewal is paid and applied. |
| `extendOrder.amount` | number | What was taken from your balance, VAT and fees included. |
| `PaymentPayload.hosted_url, paymentUrl` | string | A link to the plans page in the dashboard. Nothing to do in code, ignore them. |

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | Package not found | packageId is missing, unknown or no longer on sale. Datacenter and IPv6 need one: take it from GET /orders/:id/extend-options. |
| 400 | You can only extend with a Datacenter plan of the same size (same number of IPs and bandwidth). Only the duration can differ. | Datacenter: the package has a different IP count or bandwidth. Nothing was charged. |
| 400 | You can only renew this IPv6 plan with the same plan: same speed and the same length (30 days). | IPv6 unlimited plan: the package is a bandwidth package, a different speed or a different length. The number in brackets is your plan's own length. Nothing was charged. |
| 400 | You can only renew this IPv6 plan with an IPv6 bandwidth plan. | IPv6 bandwidth plan: the package is an unlimited one. Nothing was charged. |
| 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 plan plus the package would pass 5000 GB in total. Nothing was charged. Pick a smaller package from GET /orders/:id/extend-options. |
| 400 | Package proxy type does not match order proxy type | The package belongs to a different product than the plan. |
| 400 | This plan can't be renewed for that length right now. Please pick another one. | Custom ISP: that length isn't sold for this plan right now. Pick one from options in GET /purchase/isp-extend-price. Nothing was charged. |
| 400 | Cannot extend an expired proxy | The plan has already run out. Buy a new plan instead. |
| 400 | Cannot extend a deleted proxy plan | The plan no longer exists on the network. Buy a new plan instead. |
| 400 | Insufficient balance | Top up and try again. Nothing was charged. |
| 400 | API renewals are paid from your account balance. Send paymentMethod "Balance" and top up first if needed. | You sent a paymentMethod other than "Balance". |
| 403 | Unauthorized access to this order | The plan belongs to another account. |
| 404 | Order not found | No plan with that id. |
| 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 second renewal arrived while the first was still running. It was not charged. Wait, then check GET /orders/:id before deciding to renew 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. Nothing was charged for this request. Contact support. |
| 429 | Too many requests, Please try again after a minute | The shared 3-per-minute limit. Nothing was charged. |
| 500 | Failed to extend proxy plan. Your balance has been refunded. | The renewal did not happen and the money is back on your balance. Safe to try again later. |
| 500 | Your renewal payment went through, but we need to confirm it with a quick manual check. It will be applied within 24 hours or refunded. Please don't try again. | Datacenter, IPv6 and Custom ISP. You WERE charged and our team finishes it. Do NOT retry: a retry only gets the 409 above, so you cannot be charged twice, but it will not help either. |

#### `GET /purchase/isp-extend-price`

Custom ISP and Dedicated ISP renew at a per-IP price times the plan's IPs. Call this before renewing so you can check the balance covers it. It charges nothing.

Custom ISP: the plan's own per-IP price (your personal price when you have one) times its IPs, plus the carrier fee per IP when the plan was bought on a carrier. IP replacements are not part of a renewal. pricePerIp already includes the carrier fee. Launch discounts are for new purchases only, so a renewal is at the plan's normal price (or your personal price). totalPrice is before VAT.

A Custom ISP plan can also renew for another length than its own, for example a 1-day plan for 1 month. options lists every length you can pick, each priced exactly as POST /purchase/extend charges it. Send the periodId of the one you want as length there. The top-level fields are the plan's own length.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The ISP plan. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/purchase/isp-extend-price?orderId=3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// Custom ISP plan (5 IPs, 1 month at 3.60 per IP, bought on a carrier with a 0.35 fee)
{
  "type": "response",
  "message": "Success",
  "payload": {
    "pricePerIp": 3.95,
    "totalPrice": 19.75,
    "customRate": false,
    "ips": 5,
    "periodId": "1_month",
    "periodLabel": "1 Month",
    "days": 30,
    "options": [
      { "pricePerIp": 1.35, "totalPrice": 6.75, "customRate": false, "ips": 5, "periodId": "7_days", "periodLabel": "7 Days", "days": 7 },
      { "pricePerIp": 3.95, "totalPrice": 19.75, "customRate": false, "ips": 5, "periodId": "1_month", "periodLabel": "1 Month", "days": 30 },
      { "pricePerIp": 10.85, "totalPrice": 54.25, "customRate": false, "ips": 5, "periodId": "3_months", "periodLabel": "3 Months", "days": 90 }
    ]
  }
}

// Dedicated ISP plan
{
  "type": "response",
  "message": "Success",
  "payload": {
    "pricePerIp": 1.6,
    "totalPrice": 16,
    "customRate": false,
    "ips": 10,
    "periodId": "1m",
    "periodLabel": "1 Month",
    "days": 30
  }
}
```

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `pricePerIp` | number | Price per IP for the renewal, before VAT. |
| `totalPrice` | number | What the renewal costs before VAT. On Custom ISP it includes the carrier fee per IP when the plan has a carrier. |
| `customRate` | boolean | true when your personal price applies. Coupons are refused on the renewal then. |
| `listTotal` | number | Only when customRate is true: the total at the normal price, for comparison. |
| `ips` | number | IPs on the plan. A renewal keeps the same IPs. |
| `periodId, periodLabel` | string | The plan length. On Custom ISP periodId is "1_day", "7_days", "1_month", "2_months" or "3_months". |
| `days` | number | Days the renewal adds. |
| `options` | array | Custom ISP only: every length this plan can renew for, its own included, shortest first. Each entry has the fields above. Send the chosen periodId as length to POST /purchase/extend. |

### Your plans

#### `GET /orders`

Every active plan on the account, newest first. With an API key the stored figures are returned by default and the page size is capped at 100. Pass `liveData=true` when you want bandwidth refreshed from the live plan, which costs one lookup per order and is much slower, so keep it out of a polling loop. `extend_retired: true` marks a plan on a retired product: it works until its end date but cannot be renewed. It is false on every other plan.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `proxyType` | string | no | Only plans of this product. |
| `liveData` | boolean | no | Refresh bandwidth from the live plan. Defaults to false for API keys. Slow: one lookup per order. |
| `page` | number | no | Page number. Default 1. |
| `pageSize` | number | no | Results per page. Default 10, capped at 100 for API keys. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/orders?pageSize=25" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "orders": [
      {
        "id": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
        "package_title": "Standard Resi 1GB V2",
        "package_proxyType": "v2resi",
        "package_bandwidth": 1,
        "package_days": 90,
        "status": "CONFIRMED",
        "expire": "2026-12-03T12:00:00.000Z",
        "auto_extend": false,
        "auto_extend_mode": null,
        "auto_extend_package_id": null,
        "auto_extend_every_days": null,
        "auto_extend_next_at": null,
        "expiry_notice_enabled": false,
        "auto_extend_available": true,
        "extend_retired": false,
        "proxy": {
          "bandwidth_left": 0.87,
          "expired_at": "2026-12-03T12:00:00.000Z"
        }
      }
    ]
  },
  "pagination": { "page": 1, "pageSize": 25, "Total": 3, "Pages": 1 }
}
```

#### `GET /orders/:id`

The plan plus its proxy credentials. This is what you call after buying, and after renewing to see the new end date. Bandwidth figures are refreshed on every call. `locked: true` means the plan has run out and the credentials are withheld. The plan's real end date is `proxy.expired_at`.

Datacenter plans also carry `ip_list` (your IPs, grouped by country and then city), `country_list` (how many IPs you have per country) and `rotating`. `rotating.endpoints` is the rotating host for each country of your plan (plus `WW` when the plan owns all five countries), picked for this plan, so read it from here instead of hardcoding it. `rotating.bound_ip_conflicts` lists the countries where rotating keeps giving the same IP while an IP is whitelisted.

For Custom ISP and Dedicated ISP the per-IP lines come from GET /generator/isp instead.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id. |

Request:

```bash
curl https://catproxies.com/api/v1/user/orders/3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904 \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
// Standard Residential plan
{
  "type": "response",
  "message": "success",
  "payload": {
    "order": {
      "id": "3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904",
      "package_title": "Standard Resi 1GB V2",
      "package_proxyType": "v2resi",
      "package_bandwidth": 1,
      "status": "CONFIRMED",
      "expire": "2026-12-03T12:00:00.000Z",
      "locked": false,
      "extend_retired": false,
      "proxy": {
        "server_ip": null,
        "username": "mtpeqbcevnemewc201120",
        "password": "jonxybfeuu",
        "used_bandwidth": 0.13,
        "bandwidth_left": 0.87,
        "whitelist_ip": [],
        "expired_at": "2026-12-03T12:00:00.000Z",
        "isLiveData": true
      }
    }
  }
}

// Datacenter plan (the proxy part carries the IPs and the rotating hosts)
{
  "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",
      "locked": false,
      "proxy": {
        "username": "dcuser482913",
        "password": "dcpass91827",
        "used_bandwidth": 12.4,
        "bandwidth_left": 237.6,
        "whitelist_ip": [],
        "expired_at": "2026-10-11T10:00:00.000Z",
        "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": ["198.51.100.14", "..."] }]
          }
        ],
        "country_list": { "countries": { "US": 60, "DE": 40 }, "total": 100 },
        "rotating": {
          "endpoints": { "US": "dcus.catproxies.com", "DE": "dcde.catproxies.com" },
          "bound_ip_conflicts": []
        },
        "isLiveData": true
      }
    }
  }
}
```

#### `GET /generator/isp`

Custom ISP and Dedicated ISP are sold per IP, and every IP is its own proxy line. This returns them all. `stale: true` means the list came from our last known good copy rather than a fresh lookup; the lines are still valid, so call again shortly for the current one. Straight after buying you may instead get HTTP 202 with `{ pending: true }`, which means the plan is still being set up: wait a few seconds and retry.

Custom ISP lines all use one connection server and one port (1337) for both HTTP and SOCKS5, so port_http and port_socks are the same. The host is the connection server you picked with `region`: `auto` (the default) is the one closest to you, and connection_regions lists the others. Each line's login is tied to its IP, and ip is the address the line exits from. The payload also says how many IP replacements the plan has left (replacements) and whether it is a Clean Score plan (clean_score).

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | yes | The ISP plan. |
| `region` | string | no | Custom ISP only. The connection server: "auto" (default, closest to you) or one of the ids in connection_regions, for example "us-east" or "de". An unknown value falls back to "auto". |

Request:

```bash
# Dedicated ISP
curl "https://catproxies.com/api/v1/user/generator/isp?orderId=3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904" \
  -H "Authorization: Bearer cp_your_api_key_here"

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

Response:

```json
// Dedicated ISP plan
{
  "type": "response",
  "message": "success",
  "payload": {
    "stale": false,
    "auth_mode": "logpas",
    "location": null,
    "quantity": 5,
    "expires_at": "2026-10-04T12:00:00.000Z",
    "proxies": [
      {
        "host": "203.0.113.24",
        "port_http": 61234,
        "port_socks": 62234,
        "login": "isp_7f2a91",
        "password": "Vh3JdQuN"
      }
    ]
  }
}

// Custom ISP plan (2 IPs, Clean Score, 2 replacements bought, 1 used)
{
  "type": "response",
  "message": "success",
  "payload": {
    "stale": false,
    "auth_mode": "logpas",
    "location": "United States - New York City",
    "quantity": 2,
    "expires_at": "2026-11-12T12:00:00.000Z",
    "proxies": [
      {
        "host": "us-east.isp.catproxies.com",
        "port_http": 1337,
        "port_socks": 1337,
        "login": "cpx8f31a2-203.0.113.24",
        "password": "Rk4pX9wq",
        "ip": "203.0.113.24"
      },
      {
        "host": "us-east.isp.catproxies.com",
        "port_http": 1337,
        "port_socks": 1337,
        "login": "cpx8f31a2-203.0.113.57",
        "password": "Rk4pX9wq",
        "ip": "203.0.113.57"
      }
    ],
    "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
  }
}
```

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `proxies[].host, port_http, port_socks` | string, number | Where to connect. On Custom ISP the host is the connection server and both ports are 1337. |
| `proxies[].login, password` | string | Credentials for that line. On Custom ISP the password is the same on every line and the login carries the line's IP. |
| `proxies[].ip` | string | Custom ISP: the IP this line exits from. Send it to POST /custom-isp/orders/:id/replace to swap it. |
| `location` | object \| string \| null | Custom ISP: the city as "Country - City". Dedicated ISP: null. |
| `connection_region` | string | Custom ISP: the connection server these lines use. |
| `connection_regions` | array | Custom ISP: every connection server as { id, label }. Pass an id as region. |
| `replacements` | object | Custom ISP: { bought, used, left, pending }. pending is true while a replacement is being confirmed. |
| `clean_score` | boolean | Custom ISP: true on a Clean Score (premium tier) plan. |
| `stale` | boolean | true when the list is our last known good copy. The lines still work. |

#### `POST /custom-isp/orders/:id/replace`

Replaces one IP of a Custom ISP plan with a new IP in the same city. The country, city and UDP support stay the same; the carrier can differ. It uses one of the IP replacements bought with the plan (GET /generator/isp shows how many are left), so it only works while replacements.left is above 0.

The swap usually finishes within the call and answers 200 with the old IP, the new IP and how many replacements are left. Read GET /generator/isp afterwards for the updated lines. Sometimes the swap needs longer to confirm: then the answer is HTTP 202 with `{ checking: true }`. Do not send it again. Our team checks it, and GET /generator/isp shows replacements.pending until it is done.

One replacement at a time per plan, with about a minute between two of them. The plan must be active and not expired.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id of a Custom ISP plan. |

**Body**

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

Request:

```bash
curl -X POST https://catproxies.com/api/v1/user/custom-isp/orders/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 }
}

// Still being confirmed (HTTP 202)
{
  "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 }
}
```

**Response fields**

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

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 400 | Pick the IP you want to replace. | ip is missing. |
| 400 | You have no IP replacements left on this plan. | All replacements bought with the plan are used. Replacements cannot be added through the API. |
| 400 | This IP is no longer on your plan. Refresh the page and try again. | The IP is not on the plan any more. Read GET /generator/isp again. |
| 400 | IP replacements are only available on this ISP product. | The plan is not a Custom ISP plan. |
| 400 | This plan has expired. | Expired plans 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 | Plan not found. | No plan 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 /orders/:id/reset-password`

Issues a new password for the plan. Available on Standard Residential, Premium Residential, IPv6, Datacenter and Rotating Mobile; Custom ISP and Dedicated ISP do not support it and answer 400. The switch-over is not instant: the new password becomes active within a few minutes, and until then the old one may still work on some locations. Rebuild any proxy strings with the new one. On Standard Residential, IPv6 and Datacenter the flags live on the username, so only the password half changes (on Datacenter the one new password is shared by every IP); on Premium Residential they hang off the password, so re-append them to the new one.

Rotating Mobile works differently: the plan gets a whole new login and your remaining data moves onto it, so the USERNAME changes too. The response then carries both, as payload.username and payload.password. The old login stops working straight away, and your used and total GB carry over. One reset per plan per hour.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id. |

Request:

```bash
curl -X POST https://catproxies.com/api/v1/user/orders/3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904/reset-password \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "Proxy password reset successfully",
  "payload": { "password": "n3wp4ssw0rd" }
}
```

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 429 | Too many requests. Please wait a moment. | More than 3 purchases, renewals or resets in a minute on your account (one shared limit). Wait a minute and try again. |
| 404 | Order not found | No plan with that id on your account. |
| 400 | Password reset is only available for Standard, Premium, Unlimited Residential, IPv6, Datacenter and Mobile plans | The plan 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 can't 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 renewed in the last 5 minutes. Wait and try again. |
| 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 your login is unchanged. Safe to retry. |
| 400 | This plan has expired. | Expired plans cannot be reset. |
| 400 | This plan 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 /orders/:id before retrying. |

#### `GET /orders/:id/stats`

Read-only usage analytics for one plan: traffic and requests over time, a log of recent connections, and the sites you used most. Works on Datacenter and IPv6 plans. It never changes the plan and costs nothing.

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 you 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 we could not reach the stats source 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 /orders/:id/unlimited-metrics is the older name of this endpoint. It takes the same parameters and returns exactly the same thing.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id of a Datacenter or IPv6 plan. |

**Query parameters**

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

Request:

```bash
# Traffic over the last week, one row per day
curl "https://catproxies.com/api/v1/user/orders/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 "https://catproxies.com/api/v1/user/orders/c5c7c6b5-e488-420c-b436-e90e86031751/stats?view=activity&timeframe=1hour&page=2" \
  -H "Authorization: Bearer cp_your_api_key_here"

# The sites you used most today
curl "https://catproxies.com/api/v1/user/orders/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": {}
}
```

**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 (your side). |
| `pagination` | object | activity only. 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. |

**Errors**

| Status | Message | What it means |
| --- | --- | --- |
| 404 | Order not found | No plan with that id on your account. |
| 400 | Usage statistics are available for Datacenter, IPv6 and Unlimited Residential plans only. | The plan is another product. |
| 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. |

#### `GET /orders/:id/download`

The invoice for this order, as a PDF. This is not the proxy list: for that use GET /orders/:id, or GET /generator/isp on an ISP plan. Returns 400 when the order has no invoice, which is normal for an order paid from balance.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id. |

Request:

```bash
curl -OJ https://catproxies.com/api/v1/user/orders/3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904/download \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
(binary PDF, Content-Type: application/pdf)

When the order has no invoice:
{ "type": "error", "message": "No invoice available for this order" }
```

### Targeting options

#### `GET /generator/budget/targeting-options`

The country, state, city and ASN values you can put in a Standard Residential username. Pass the order so the list matches that plan. The tree is large and changes rarely, so cache it rather than fetching it per request.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `orderId` | string | no | A Standard Residential plan. Without it you get the general list. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/generator/budget/targeting-options?orderId=3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904" \
  -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": "losangeles", "name": "Los Angeles" }
            ]
          }
        ]
      }
    ]
  }
}
```

#### `GET /generator/mobile-locations/countries`

Countries with live mobile coverage. Use the codes exactly as returned. The lists are cached on our side for an hour and change rarely, so fetch them once at startup rather than per request.

Request:

```bash
curl https://catproxies.com/api/v1/user/generator/mobile-locations/countries \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": { "countries": [ { "code": "US", "name": "United States" } ] }
}
```

#### `GET /generator/mobile-locations/regions`

Regions or states you can target inside a mobile country. Full names in lowercase, not two-letter codes.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `country` | string | yes | Country code from the countries endpoint. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/generator/mobile-locations/regions?country=US" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": { "regions": [{ "code": "california", "name": "California" }] }
}
```

#### `GET /generator/mobile-locations/cities`

Cities you can target. Spaces are written as a plus sign, never a hyphen.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `country` | string | yes | Country code. |
| `region` | string | no | Region code. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/generator/mobile-locations/cities?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" }] }
}
```

#### `GET /generator/mobile-locations/isps`

Carriers available for a country, and optionally a city. Pin a carrier at country level for the most reliable results; carrier coverage in one particular city moves around.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `country` | string | yes | Country code. |
| `region` | string | no | Region code. |
| `city` | string | no | City code. |

Request:

```bash
curl "https://catproxies.com/api/v1/user/generator/mobile-locations/isps?country=US" \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": { "isps": [{ "code": "at&t+wireless", "name": "AT&T Wireless" }] }
}
```

### Automatic renewal

#### `GET /orders/:id/auto-extend`

Whether this plan renews itself from your balance, in which mode, and whether you asked for the heads-up email. `supportsMode` is true on the products that offer a choice of mode. `renewable` says whether automatic renewal can be switched on for this plan. It is false on Datacenter, IPv6 and plans on a retired product: automatic renewal is not offered for them (Datacenter and IPv6 plans can still be renewed by hand with POST /purchase/extend). `packages` lists the refill sizes you may choose from: exactly the ones on sale for that product.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id. |

Request:

```bash
curl https://catproxies.com/api/v1/user/orders/3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904/auto-extend \
  -H "Authorization: Bearer cp_your_api_key_here"
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": {
    "notice": false,
    "enabled": true,
    "mode": "schedule",
    "packageId": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f",
    "everyDays": 30,
    "nextAt": "2026-10-09T12:00:00.000Z",
    "supportsMode": true,
    "renewable": true,
    "packages": [
      { "id": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f", "title": "Standard Resi 1GB", "bandwidth": 1, "days": 90, "price": 2.5 }
    ]
  }
}
```

#### `PATCH /orders/:id/auto-extend`

Changes either switch. Every field is optional: send only what you are changing.

`notice` turns the "your plan runs out soon" email on or off. It is off by default and no money is involved.

`enabled` turns automatic renewal on or off. On Residential and Mobile you also choose a mode. "term" buys the smallest package we sell, a couple of days before the plan expires, which is the cheapest way to keep it alive. "schedule" buys a size you pick every `everyDays` days, and needs `packageId` (one of the ids from `packages`) and `everyDays` (1 to 180). Custom ISP and Dedicated ISP renew one way only, so the mode is ignored there. Datacenter, IPv6 and plans on a retired product are refused, and so is any plan that has ended, belongs to a reseller account, or has a term of two days or less (`auto_extend_available` on the plans list tells you in advance). Switching renewal off keeps your chosen size and interval, so switching it back on restores them.

Nothing is charged when you call this. The balance is only taken at the moment a renewal actually runs, and if it is short then that renewal simply fails.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Order id. |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | yes | On or off. |
| `notice` | boolean | no | Turn the "runs out soon" email on or off for this plan. Independent of renewal. |
| `mode` | string | no | "term" or "schedule". Used when enabling a Residential or Mobile plan; defaults to "term". |
| `packageId` | string | no | Required with mode "schedule". One of the ids from `packages` on the GET. |
| `everyDays` | integer | no | Required with mode "schedule". Days between refills, 1 to 180. |

Request:

```bash
curl -X PATCH https://catproxies.com/api/v1/user/orders/3b0a7c41-2f18-4d0a-9c33-5a1e77b2c904/auto-extend \
  -H "Authorization: Bearer cp_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true, "mode": "schedule", "packageId": "cdd0c81b-43fd-4bef-9709-2173a5ad3c5f", "everyDays": 30 }'
```

Response:

```json
{
  "type": "response",
  "message": "success",
  "payload": { "notice": false, "enabled": true, "mode": "schedule", "everyDays": 30, "renewable": true, "supportsMode": true }
}
```

## Support

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