# Catalog and pricing — PapaProxy.net API reference

Page: https://papaproxy.net/developers/reference/catalog-and-pricing.php
Last updated: November 9, 2026
Base URL: `https://papaproxy.net/account_new`. Authentication and conventions: https://papaproxy.net/developers/reference/

### List available countries

`GET /api/v1/billing/countries`

- Access: No API key required

List countries with their ISO 3166-1 alpha-2 codes, numeric IDs, and the number of IPs currently available for proxy packages.

No API key required. Pass the `code` straight to `segments[].country` in quote and order requests. Countries are sorted by available IPs, most first. Defaults: `product_kind=proxy_datacenter`, `geo_source=maxmind` (use the same `geo_source` in quote and order requests). For dedicated proxies, use `GET /api/v1/billing/dedicated/countries`.

Parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `product_kind (query)` | string, nullable | no | Product: proxy_datacenter or proxy_isp |
| `geo_source (query)` | string, nullable | no | Geo source the country tags are scoped to: maxmind or ripe |

Request examples:

cURL:

```bash
curl -X GET "https://papaproxy.net/account_new/api/v1/billing/countries"
```

Python:

```python
import requests

resp = requests.get(
    "https://papaproxy.net/account_new/api/v1/billing/countries",
    timeout=30,
)
resp.raise_for_status()
print(resp.json())
```

Node.js:

```javascript
const resp = await fetch("https://papaproxy.net/account_new/api/v1/billing/countries", {
  method: "GET",
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());
```

PHP:

```php
<?php
$ch = curl_init('https://papaproxy.net/account_new/api/v1/billing/countries');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
var_dump($status, json_decode($response, true));
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, err := http.NewRequest("GET", "https://papaproxy.net/account_new/api/v1/billing/countries", nil)
	if err != nil {
		panic(err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(data))
}
```

Responses:

- `200` — Countries you can order.

Example response:

```json
{
  "geo_source": "maxmind",
  "product_kind": "proxy_datacenter",
  "countries": [
    {
      "code": "US",
      "name": "United States",
      "tag_id": 197148,
      "available": 32452
    },
    {
      "code": "RU",
      "name": "Russia",
      "tag_id": 197146,
      "available": 15616
    }
  ]
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `geo_source` | string | yes | Geo source the country list is scoped to (maxmind/ripe) |
| `product_kind` | string | yes | Product kind the availability counts are computed for |
| `countries` | array of object | no | Available countries, most stock first |
| `countries[].code` | string | yes | ISO 3166-1 alpha-2 code (e.g. 'DE', 'GB') |
| `countries[].name` | string | yes | Country name (e.g. 'Germany') |
| `countries[].tag_id` | integer | yes | Numeric country ID (the ISO code is accepted everywhere this ID is). |
| `countries[].available` | integer | yes | IPs currently available in this country |

- `422` — Validation error: a field is missing or has the wrong type or value.

  Example:

  ```json
  {"detail": "Validation error"}
  ```

- `429` — Too many requests: the per-account limit is 60 requests per minute, shared by all keys and the MCP server. Retry with exponential backoff.

  Example:

  ```json
  {"detail": "Too Many Requests"}
  ```


### Get pricing terms

`GET /api/v1/billing/composite/pricing-info`

- Access: No API key required

Volume-discount tiers, term discounts, order minimums, included concurrent connections, and add-on prices for proxy packages — everything a calculator needs to show discount thresholds before requesting a quote.

No API key required. `min_ips_per_order` (100) and `min_ips_per_segment` (10) are the package minimums; they also apply when you add IPs to a package. Dedicated proxies start at 1 IP; price them with `POST /api/v1/billing/dedicated/quote`. The figures are for display only; quote and order responses are authoritative.

Request examples:

cURL:

```bash
curl -X GET "https://papaproxy.net/account_new/api/v1/billing/composite/pricing-info"
```

Python:

```python
import requests

resp = requests.get(
    "https://papaproxy.net/account_new/api/v1/billing/composite/pricing-info",
    timeout=30,
)
resp.raise_for_status()
print(resp.json())
```

Node.js:

```javascript
const resp = await fetch("https://papaproxy.net/account_new/api/v1/billing/composite/pricing-info", {
  method: "GET",
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());
```

PHP:

```php
<?php
$ch = curl_init('https://papaproxy.net/account_new/api/v1/billing/composite/pricing-info');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
var_dump($status, json_decode($response, true));
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, err := http.NewRequest("GET", "https://papaproxy.net/account_new/api/v1/billing/composite/pricing-info", nil)
	if err != nil {
		panic(err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(data))
}
```

Responses:

- `200` — Pricing terms for display: volume-discount tiers, term discounts, order minimums, included concurrent connections, and add-on prices.

Example response:

```json
{
  "volume_tiers": [
    {
      "min_ips": 0,
      "price_millicents_per_ip": 19000,
      "label": null
    },
    {
      "min_ips": 100,
      "price_millicents_per_ip": 19000,
      "label": null
    }
  ],
  "period_discounts": {},
  "min_ips_per_segment": 10,
  "min_ips_per_order": 100,
  "conn_cap_tiers": [
    {
      "min_ips": 0,
      "max_ips": 800,
      "flat_cap": 1500,
      "multiplier": null
    },
    {
      "min_ips": 800,
      "max_ips": 5000,
      "flat_cap": null,
      "multiplier": 3
    }
  ],
  "whitelist_free_quota": 5,
  "extra_whitelist_ip_price_cents": 100,
  "free_refresh_days": 8,
  "trial": {
    "enabled": true,
    "period_days": 2,
    "tariff_coefficient": 2.0,
    "registration_window_enabled": false,
    "registration_window_days": 30,
    "max_per_month": 3
  }
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `volume_tiers` | array of object | yes | Volume-discount tiers, ascending by `min_ips`. |
| `volume_tiers[].min_ips` | integer | yes | Total-IP threshold at which this tier applies · Minimum: 0 |
| `volume_tiers[].price_millicents_per_ip` | integer | yes | Per-IP price in millicents (1/1000 of a cent) · Minimum: 0 |
| `volume_tiers[].label` | string, nullable | no | Human-readable tier label. |
| `period_discounts` | object | yes | Period-months (string key) -> whole-percentage discount |
| `min_ips_per_segment` | integer | yes | Minimum IP count for a single segment to be accepted · Minimum: 1 |
| `min_ips_per_order` | integer | yes | Minimum total IP count (summed across segments) for an order · Minimum: 1 |
| `conn_cap_tiers` | array of object | no | Included concurrent connections by IP count. |
| `conn_cap_tiers[].min_ips` | integer | yes | Inclusive lower bound of this bucket's IP range · Minimum: 0 |
| `conn_cap_tiers[].max_ips` | integer, nullable | no | Exclusive upper bound; null = no upper bound (last bucket) |
| `conn_cap_tiers[].flat_cap` | integer, nullable | no | Flat connection cap for this bucket |
| `conn_cap_tiers[].multiplier` | integer, nullable | no | Per-IP connection multiplier for this bucket |
| `whitelist_free_quota` | integer | no | Whitelist IPs included free with each service. · Default: `0` |
| `extra_whitelist_ip_price_cents` | integer | no | Monthly price per whitelist IP beyond the free allowance, in cents. · Default: `0` |
| `free_refresh_days` | integer | no | Interval, in days, between free scheduled IP list refreshes. · Default: `0` |
| `trial` | object | no | Paid 48-hour trial settings: availability, length in days, and price coefficient. |
| `trial.enabled` | boolean | no | Default: `true` |
| `trial.period_days` | integer | no | Term length in days. · Minimum: 1 · Default: `2` |
| `trial.tariff_coefficient` | number | no | Default: `2` |
| `trial.registration_window_enabled` | boolean | no | Default: `false` |
| `trial.registration_window_days` | integer | no | Minimum: 1 · Default: `30` |
| `trial.max_per_month` | integer | no | Minimum: 0 · Default: `3` |

- `429` — Too many requests: the per-account limit is 60 requests per minute, shared by all keys and the MCP server. Retry with exponential backoff.

  Example:

  ```json
  {"detail": "Too Many Requests"}
  ```


### Distribute IPs across countries

`POST /api/v1/billing/composite/auto-plan`

- Access: No API key required

Spread a total IP count across the countries that are currently in stock (the “Country mix” option). Optionally exclude countries with `excluded_countries` (ISO codes).

No API key required and no charge. `total_ip` must be at least 100 (the package minimum); a smaller value returns `422`. Pass the returned `segments` to the quote endpoint for pricing and to the order endpoint to purchase.

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `total_ip` | integer | yes | Total number of IPs to distribute. At least 100. · Minimum: 1 · Maximum: 1000000 |
| `product_kind` | string | no | Product: `proxy_datacenter` or `proxy_isp`. Dedicated proxies are priced and ordered through `/api/v1/billing/dedicated/*`. · Default: `"proxy_datacenter"` |
| `geo_source` | string | no | Geo source for country lookups · One of: `maxmind`, `ripe` · Default: `"maxmind"` |
| `excluded_countries` | array of integer or string | no | Countries to leave out, as ISO 3166-1 alpha-2 codes. |

Request examples:

cURL:

```bash
curl -X POST "https://papaproxy.net/account_new/api/v1/billing/composite/auto-plan" \
  -H "Content-Type: application/json" \
  -d '{"total_ip": 300}'
```

Python:

```python
import requests

resp = requests.post(
    "https://papaproxy.net/account_new/api/v1/billing/composite/auto-plan",
    json={
        "total_ip": 300
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json())
```

Node.js:

```javascript
const resp = await fetch("https://papaproxy.net/account_new/api/v1/billing/composite/auto-plan", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "total_ip": 300
  }),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());
```

PHP:

```php
<?php
$ch = curl_init('https://papaproxy.net/account_new/api/v1/billing/composite/auto-plan');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'total_ip' => 300
    ]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
var_dump($status, json_decode($response, true));
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "https://papaproxy.net/account_new/api/v1/billing/composite/auto-plan", strings.NewReader(`{"total_ip": 300}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(data))
}
```

Responses:

- `200` — Proposed distribution of IPs by country.

Example response:

```json
{
  "snapshot_version": "5c7f1a22-bcbf-5cf5-845f-92443fc4d544",
  "segments": [
    {
      "country": 197148,
      "country_code": "US",
      "count": 148
    },
    {
      "country": 197146,
      "country_code": "RU",
      "count": 71
    }
  ],
  "total_ip": 300,
  "countries_used": 4,
  "subnet_diversity": null
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `snapshot_version` | string (uuid) | yes | Version of the availability data used for this response. |
| `segments` | array of object | no | Proposed per-country distribution, summing to total_ip |
| `segments[].country` | integer | yes | Numeric country ID. The ISO code is in `country_code`. |
| `segments[].country_code` | string, nullable | no | ISO 3166-1 alpha-2 country code (e.g. "DE"). |
| `segments[].count` | integer | yes | Number of IPs allocated to this country · Minimum: 1 |
| `total_ip` | integer | yes | Total IP count across all segments (echoes the request) |
| `countries_used` | integer | yes | Number of distinct countries in the plan |
| `subnet_diversity` | integer, nullable | no | Number of distinct /24 subnets in the proposed allocation. Available for ISP proxies only; null for other products. |

- `422` — Validation error, or `total_ip` is below 100.

  Example:

  ```json
  {"detail": "Validation error"}
  ```

- `429` — Too many requests: the per-account limit is 60 requests per minute, shared by all keys and the MCP server. Retry with exponential backoff.

  Example:

  ```json
  {"detail": "Too Many Requests"}
  ```


### Get a price quote

`POST /api/v1/billing/composite/quote`

- Access: API key optional

Price a set of segments (`[{country, count}]`) for a proxy package and term. The quote is non-binding (`binding=false`).

- The total must be at least 100 IPs; otherwise the response is `422`.
- A segment that can't be fulfilled — fewer than 10 IPs, or more IPs than are in stock — doesn't fail the request: it's listed in `errors` and left out of `segments` and the total. Always check `errors` before you order.
- An invalid `promo_code` returns `422` (`Invalid promo code.`).

The API key is optional. With a key from an account that has a partner discount, the response also includes `retail_*` and `partner_discount_*` fields; otherwise those fields are null.

Pricing: the per-IP rate depends on the total IP count across all segments (volume tiers); term discounts apply on top. See `GET /api/v1/billing/composite/pricing-info` for the tiers.

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `segments` | array of object | yes | One or more segments forming the mix · Min items: 1 |
| `segments[].country` | integer or string | yes | ISO 3166-1 alpha-2 country code ("DE", "GB"). A numeric country ID from `GET /api/v1/billing/countries` is also accepted. |
| `segments[].count` | integer | yes | Number of IPs in this country. At least 10; the order total must be at least 100. · Minimum: 1 · Maximum: 100000 |
| `product_kind` | string | no | Product: `proxy_datacenter` or `proxy_isp`. Dedicated proxies are priced and ordered through `/api/v1/billing/dedicated/*`. · Default: `"proxy_datacenter"` |
| `period_months` | integer | no | Term length in months (1, 3, 6, or 12). Longer terms get a discount. Default: 1. |
| `geo_source` | string | no | Geo source for country lookups: maxmind (geolocation) or ripe (RIR allocation). Default maxmind. · One of: `maxmind`, `ripe` |
| `promo_code` | string, nullable | no | Optional promo code, applied after volume and term discounts. · Max length: 64 |
| `trial` | boolean | no | Price the paid 48-hour trial instead of a full term. · Default: `false` |
| `extra_whitelist_count` | integer | no | Paid whitelist IPs beyond the free allowance. · Minimum: 0 · Maximum: 10000 · Default: `0` |
| `udp_unlock` | boolean | no | Include the UDP add-on (a percentage of the base price). · Default: `false` |
| `extra_connections_count` | integer | no | Paid concurrent connections beyond those included, priced into the quote. For an existing service, buy the `extra_connections` add-on instead. · Minimum: 0 · Maximum: 10000 · Default: `0` |

Request examples:

cURL:

```bash
curl -X POST "https://papaproxy.net/account_new/api/v1/billing/composite/quote" \
  -H "Content-Type: application/json" \
  -d '{"segments": [{"country": "US", "count": 100}], "period_months": 1}'
```

Python:

```python
import requests

resp = requests.post(
    "https://papaproxy.net/account_new/api/v1/billing/composite/quote",
    json={
        "segments": [
            {
                "country": "US",
                "count": 100
            }
        ],
        "period_months": 1
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json())
```

Node.js:

```javascript
const resp = await fetch("https://papaproxy.net/account_new/api/v1/billing/composite/quote", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "segments": [
      {
        "country": "US",
        "count": 100
      }
    ],
    "period_months": 1
  }),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());
```

PHP:

```php
<?php
$ch = curl_init('https://papaproxy.net/account_new/api/v1/billing/composite/quote');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'segments' => [[
            'country' => 'US',
            'count' => 100
        ]],
        'period_months' => 1
    ]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
var_dump($status, json_decode($response, true));
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	req, err := http.NewRequest("POST", "https://papaproxy.net/account_new/api/v1/billing/composite/quote", strings.NewReader(`{"segments": [{"country": "US", "count": 100}], "period_months": 1}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(data))
}
```

Responses:

- `200` — Price quote. Check `errors` for segments that were left out.

Example response:

```json
{
  "snapshot_version": "5c7f1a22-bcbf-5cf5-845f-92443fc4d544",
  "built_at": "2026-10-30T04:00:18.702558Z",
  "binding": false,
  "segments": [
    {
      "country": 197148,
      "country_code": "US",
      "count": 100,
      "unit_base_cents": 19,
      "type_multiplier": 1.0,
      "geo_multiplier": 1.0,
      "segment_price_cents": 1900,
      "available": true
    }
  ],
  "errors": [],
  "total_ip": 100,
  "total_price_cents": 1900,
  "unit_base_cents": 19,
  "period_months": 1,
  "discount_pct": 0,
  "volume_discount_pct": 0,
  "monthly_price_cents": 1900,
  "retail_total_price_cents": null,
  "retail_monthly_price_cents": null,
  "partner_discount_percent": null,
  "partner_discount_cents": null,
  "promo_code_applied": null,
  "promo_discount_cents": 0,
  "total_before_promo_cents": null,
  "trial": false,
  "trial_period_days": null,
  "trial_coefficient": null,
  "addon_monthly_cents": 0,
  "addon_breakdown": [],
  "trial_price_cents": null,
  "extra_whitelist_count": 0,
  "extra_connections_count": 0,
  "udp_unlock": false
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `snapshot_version` | string (uuid) | yes | Version of the availability data used for this quote. |
| `built_at` | string (date-time), nullable | no | When the availability data was last updated (UTC). |
| `binding` | boolean | no | Always false — quotation is non-binding · Default: `false` |
| `segments` | array of object | no | Per-segment pricing breakdown |
| `segments[].country` | integer | yes | Numeric country ID. The ISO code is in `country_code`. |
| `segments[].country_code` | string, nullable | no | ISO 3166-1 alpha-2 country code (e.g. "DE"). |
| `segments[].count` | integer | yes |  |
| `segments[].unit_base_cents` | integer | yes | Volume-tier base unit price in integer cents per IP |
| `segments[].type_multiplier` | number | yes | Product multiplier applied to the base rate. |
| `segments[].geo_multiplier` | number | yes | Country multiplier (default 1.0). |
| `segments[].segment_price_cents` | integer | yes | Segment price: count × base rate × product multiplier × country multiplier, rounded up to whole cents. |
| `segments[].available` | boolean | yes | Whether enough IPs are available for this segment. |
| `errors` | array of object | no | Errors for unavailable/invalid segments |
| `errors[].country` | integer | yes | Numeric country ID of the segment that was left out. |
| `errors[].message` | string | yes |  |
| `total_ip` | integer | no | Total IP count across all valid segments · Default: `0` |
| `total_price_cents` | integer | no | Discounted total charged for the full period in integer cents. Equals monthly_price_cents x period_months x (1 - discount). · Default: `0` |
| `unit_base_cents` | integer | no | Volume-tier base unit price applied (cents per IP) · Default: `0` |
| `period_months` | integer | no | Billing period length in months (1/3/6/12) · Default: `1` |
| `discount_pct` | integer | no | Term discount as a whole percentage. The volume discount is reported in `volume_discount_pct`. · Default: `0` |
| `volume_discount_pct` | integer | no | Volume discount as a whole percentage, for display. Term discount is reported separately in `discount_pct`; both are already included in `total_price_cents`. · Default: `0` |
| `monthly_price_cents` | integer | no | Undiscounted monthly total (sum of segment prices) in integer cents · Default: `0` |
| `retail_total_price_cents` | integer, nullable | no | Total before the partner discount, in cents. Null unless a partner discount applies. |
| `retail_monthly_price_cents` | integer, nullable | no | Monthly price before the partner discount, in cents. Null unless a partner discount applies. |
| `partner_discount_percent` | integer, nullable | no | Partner discount percentage, when it applies. |
| `partner_discount_cents` | integer, nullable | no | Partner discount amount in cents, when it applies. |
| `promo_code_applied` | string, nullable | no | Promo code applied to this quote, if any. |
| `promo_discount_cents` | integer | no | Absolute promo discount in integer cents applied to total_price_cents. · Minimum: 0 · Default: `0` |
| `total_before_promo_cents` | integer, nullable | no | Pre-promo total in integer cents when a promo was applied; null when no promo. |
| `trial` | boolean | no | True when the quote is for the paid trial. · Default: `false` |
| `trial_period_days` | integer, nullable | no | Trial length in days; null for regular quotes. |
| `trial_coefficient` | number, nullable | no | Price coefficient of the trial; null for regular quotes. |
| `addon_monthly_cents` | integer | no | Monthly cents from pre-purchase add-ons (whitelist + UDP + connections). · Minimum: 0 · Default: `0` |
| `addon_breakdown` | array of object | no | Per-addon monthly line-items. Sum of monthly_cents equals addon_monthly_cents. |
| `addon_breakdown[].addon_type` | string | yes | Add-on type. · One of: `extra_whitelist_ip`, `udp_unlock`, `extra_connections` |
| `addon_breakdown[].quantity` | integer | yes | Number of units (IPs or connections; 1 for UDP). · Minimum: 0 |
| `addon_breakdown[].unit_price_cents` | integer | yes | Per-unit monthly cents · Minimum: 0 |
| `addon_breakdown[].monthly_cents` | integer | yes | Monthly cost: quantity × unit price (UDP is a percentage of the base price). · Minimum: 0 |
| `trial_price_cents` | integer, nullable | no | Trial price in cents (before promo); null for regular quotes. |
| `extra_whitelist_count` | integer | no | Paid whitelist slots included in this quote. · Minimum: 0 · Default: `0` |
| `extra_connections_count` | integer | no | Pre-purchase extra concurrent connections included in this quote. · Minimum: 0 · Default: `0` |
| `udp_unlock` | boolean | no | Whether UDP unlock was included in this quote. · Default: `false` |

- `422` — Validation error, fewer than 100 IPs in total, or an invalid promo code.

  Example:

  ```json
  {"detail": "Invalid promo code."}
  ```

- `429` — Too many requests: the per-account limit is 60 requests per minute, shared by all keys and the MCP server. Retry with exponential backoff.

  Example:

  ```json
  {"detail": "Too Many Requests"}
  ```


### List products

`GET /api/v1/customer/catalog`

- Access: API key required · scope `catalog:read`

The product catalog with your prices.

Parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `offset (query)` | integer | no | Number of items to skip. · Minimum: 0 · Default: `0` |
| `limit (query)` | integer | no | Page size. · Minimum: 1 · Maximum: 100 · Default: `50` |

Request examples:

cURL:

```bash
curl -X GET "https://papaproxy.net/account_new/api/v1/customer/catalog" \
  -H "Authorization: Bearer $PAPAPROXY_API_KEY"
```

Python:

```python
import os

import requests

resp = requests.get(
    "https://papaproxy.net/account_new/api/v1/customer/catalog",
    headers={"Authorization": f"Bearer {os.environ['PAPAPROXY_API_KEY']}"},
    timeout=30,
)
resp.raise_for_status()
print(resp.json())
```

Node.js:

```javascript
const resp = await fetch("https://papaproxy.net/account_new/api/v1/customer/catalog", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PAPAPROXY_API_KEY}`,
  },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());
```

PHP:

```php
<?php
$ch = curl_init('https://papaproxy.net/account_new/api/v1/customer/catalog');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('PAPAPROXY_API_KEY'),
    ],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
var_dump($status, json_decode($response, true));
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://papaproxy.net/account_new/api/v1/customer/catalog", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PAPAPROXY_API_KEY"))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(data))
}
```

Responses:

- `200` — Paginated product list.

Example response:

```json
{
  "products": [
    {
      "id": "5234db61-2f19-5aa0-8e64-faa72f7d4b76",
      "kind": "proxy_rotating",
      "billing_model": "metered_requests",
      "name": "Starter 2.5M",
      "description": "2.5 million requests over 30 days, about 83,000 a day. New datacenter IPv4 from a 100,000+ pool on every request; sticky sessions keep one IP when you need it. Unlimited bandwidth.",
      "params": {
        "ip_count": null,
        "geo": null,
        "protocol": null,
        "rate_limit_rps": null,
        "sticky": true,
        "data_limit_bytes": null,
        "traffic_gb": null,
        "pool_class": "datacenter",
        "devices": null,
        "period_days": 30,
        "request_package_size": 2500000
      },
      "price_cents": 4900,
      "price_unit": "period",
      "price_book_id": "1f4229e5-ac7e-50ae-bc7c-2680e501a91f",
      "is_active": true,
      "features": []
    },
    {
      "id": "79001ddf-8570-583a-9045-0511848550e4",
      "kind": "proxy_rotating",
      "billing_model": "metered_requests",
      "name": "Standard 5M",
      "description": "5 million requests over 30 days, about 167,000 a day. New datacenter IPv4 from a 100,000+ pool on every request; sticky sessions keep one IP when you need it. Unlimited bandwidth.",
      "params": {
        "ip_count": null,
        "geo": null,
        "protocol": null,
        "rate_limit_rps": null,
        "sticky": true,
        "data_limit_bytes": null,
        "traffic_gb": null,
        "pool_class": "datacenter",
        "devices": null,
        "period_days": 30,
        "request_package_size": 5000000
      },
      "price_cents": 7900,
      "price_unit": "period",
      "price_book_id": "1f4229e5-ac7e-50ae-bc7c-2680e501a91f",
      "is_active": true,
      "features": []
    }
  ],
  "total": 6,
  "offset": 0,
  "limit": 2,
  "price_book_kind": "retail"
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `products` | array of object | yes |  |
| `products[].id` | string (uuid) | yes | Unique ID. |
| `products[].kind` | string | yes | Product type, for example `proxy_rotating`. |
| `products[].billing_model` | string | yes |  |
| `products[].name` | string | yes |  |
| `products[].description` | string, nullable | yes |  |
| `products[].params` | object, nullable | yes | Product parameters; they vary by product type (for example IP count, locations, term, request allowance, or session settings). |
| `products[].params.ip_count` | integer, nullable | no | Number of IPs. |
| `products[].params.geo` | array of string, nullable | no |  |
| `products[].params.protocol` | string, nullable | no |  |
| `products[].params.rate_limit_rps` | integer, nullable | no |  |
| `products[].params.sticky` | boolean, nullable | no |  |
| `products[].params.data_limit_bytes` | integer, nullable | no | Data allowance in bytes, for products with a data allowance. |
| `products[].params.traffic_gb` | number, nullable | no | Data allowance in GB, for products with a data allowance. |
| `products[].params.pool_class` | string, nullable | no | IP pool type of the product. · One of: `datacenter`, `isp` |
| `products[].params.devices` | integer, nullable | no |  |
| `products[].params.period_days` | integer, nullable | no | Term length in days. |
| `products[].params.request_package_size` | integer, nullable | no | Prepaid request count for rotating proxies (for example 2,500,000). |
| `products[].price_cents` | integer, nullable | no | Resolved retail price in cents; null if none |
| `products[].price_unit` | string, nullable | no |  |
| `products[].price_book_id` | string (uuid), nullable | no | ID of the price list the price comes from. |
| `products[].is_active` | boolean | yes | Whether it's active. |
| `products[].features` | array of string | no | Product features. |
| `total` | integer | yes | Total number of items across all pages. |
| `offset` | integer | no | Number of items skipped. · Default: `0` |
| `limit` | integer | no | Page size. · Default: `50` |
| `price_book_kind` | string | no | One of: `retail`, `wholesale` · Default: `"retail"` |

- `401` — The API key is missing, malformed, revoked, or expired, or the account is inactive.

  Example:

  ```json
  {"detail": "Invalid or revoked API key."}
  ```

- `403` — The API key lacks scope `catalog:read`.

  Example:

  ```json
  {"detail": "API key is missing required scope: catalog:read"}
  ```

- `422` — Validation error: a field is missing or has the wrong type or value.

  Example:

  ```json
  {"detail": "Validation error"}
  ```

- `429` — Too many requests: the per-account limit is 60 requests per minute, shared by all keys and the MCP server. Retry with exponential backoff.

  Example:

  ```json
  {"detail": "Too Many Requests"}
  ```


### Get a product

`GET /api/v1/customer/catalog/{product_id}`

- Access: API key required · scope `catalog:read`

Product details with your price.

Parameters:

| Field | Type | Required | Description |
|---|---|---|---|
| `product_id (path)` | string (uuid) | yes | ID of the product. |

Request examples:

cURL:

```bash
curl -X GET "https://papaproxy.net/account_new/api/v1/customer/catalog/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -H "Authorization: Bearer $PAPAPROXY_API_KEY"
```

Python:

```python
import os

import requests

resp = requests.get(
    "https://papaproxy.net/account_new/api/v1/customer/catalog/3fa85f64-5717-4562-b3fc-2c963f66afa6",
    headers={"Authorization": f"Bearer {os.environ['PAPAPROXY_API_KEY']}"},
    timeout=30,
)
resp.raise_for_status()
print(resp.json())
```

Node.js:

```javascript
const resp = await fetch("https://papaproxy.net/account_new/api/v1/customer/catalog/3fa85f64-5717-4562-b3fc-2c963f66afa6", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.PAPAPROXY_API_KEY}`,
  },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());
```

PHP:

```php
<?php
$ch = curl_init('https://papaproxy.net/account_new/api/v1/customer/catalog/3fa85f64-5717-4562-b3fc-2c963f66afa6');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('PAPAPROXY_API_KEY'),
    ],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
var_dump($status, json_decode($response, true));
```

Go:

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "https://papaproxy.net/account_new/api/v1/customer/catalog/3fa85f64-5717-4562-b3fc-2c963f66afa6", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PAPAPROXY_API_KEY"))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.Status, string(data))
}
```

Responses:

- `200` — A catalog product.

Example response:

```json
{
  "id": "5234db61-2f19-5aa0-8e64-faa72f7d4b76",
  "kind": "proxy_rotating",
  "billing_model": "metered_requests",
  "name": "Starter 2.5M",
  "description": "2.5 million requests over 30 days, about 83,000 a day. New datacenter IPv4 from a 100,000+ pool on every request; sticky sessions keep one IP when you need it. Unlimited bandwidth.",
  "params": {
    "ip_count": null,
    "geo": null,
    "protocol": null,
    "rate_limit_rps": null,
    "sticky": true,
    "data_limit_bytes": null,
    "traffic_gb": null,
    "pool_class": "datacenter",
    "devices": null,
    "period_days": 30,
    "request_package_size": 2500000
  },
  "price_cents": 4900,
  "price_unit": "period",
  "price_book_id": "1f4229e5-ac7e-50ae-bc7c-2680e501a91f",
  "is_active": true,
  "features": []
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes | Unique ID. |
| `kind` | string | yes | Product type, for example `proxy_rotating`. |
| `billing_model` | string | yes |  |
| `name` | string | yes |  |
| `description` | string, nullable | yes |  |
| `params` | object, nullable | yes | Product parameters; they vary by product type (for example IP count, locations, term, request allowance, or session settings). |
| `params.ip_count` | integer, nullable | no | Number of IPs. |
| `params.geo` | array of string, nullable | no |  |
| `params.protocol` | string, nullable | no |  |
| `params.rate_limit_rps` | integer, nullable | no |  |
| `params.sticky` | boolean, nullable | no |  |
| `params.data_limit_bytes` | integer, nullable | no | Data allowance in bytes, for products with a data allowance. |
| `params.traffic_gb` | number, nullable | no | Data allowance in GB, for products with a data allowance. |
| `params.pool_class` | string, nullable | no | IP pool type of the product. · One of: `datacenter`, `isp` |
| `params.devices` | integer, nullable | no |  |
| `params.period_days` | integer, nullable | no | Term length in days. |
| `params.request_package_size` | integer, nullable | no | Prepaid request count for rotating proxies (for example 2,500,000). |
| `price_cents` | integer, nullable | no | Resolved retail price in cents; null if none |
| `price_unit` | string, nullable | no |  |
| `price_book_id` | string (uuid), nullable | no | ID of the price list the price comes from. |
| `is_active` | boolean | yes | Whether it's active. |
| `features` | array of string | no | Product features. |

- `401` — The API key is missing, malformed, revoked, or expired, or the account is inactive.

  Example:

  ```json
  {"detail": "Invalid or revoked API key."}
  ```

- `403` — The API key lacks scope `catalog:read`.

  Example:

  ```json
  {"detail": "API key is missing required scope: catalog:read"}
  ```

- `404` — Product not found, or it doesn't belong to your account.
- `422` — Validation error: a field is missing or has the wrong type or value.

  Example:

  ```json
  {"detail": "Validation error"}
  ```

- `429` — Too many requests: the per-account limit is 60 requests per minute, shared by all keys and the MCP server. Retry with exponential backoff.

  Example:

  ```json
  {"detail": "Too Many Requests"}
  ```

