ProxioDocs
API Reference

Orders

List, quote, and place Proxio orders. GET /orders and /orders/{id} read your order history, POST /orders/quote prices one without buying it, POST /orders places a wallet-paid purchase, and POST /services/{id}/renew tops up or extends a service. Pricing is server-authoritative and both purchase writes require an idempotency key.

Orders are how you buy and renew capacity. Listing and reading orders needs read; placing and renewing spends from your wallet and needs the purchase scope plus a required idempotency key.

Wallet-paid only

The API places wallet-paid orders. Top up your wallet first (card and crypto top-ups stay on the web checkout). Server-side pricing is always authoritative, any prices you send in the body are ignored.

List orders

GET /orders, cursor-paginated, filterable by status and category.

Query parameters

ParameterValuesNotes
statusPENDING, PAID, CANCELED, REFUNDED, EXPIREDCase-insensitive. One value, not a list. Any other value fails with VALIDATION_ERROR.
categoryRESIDENTIAL, ISP, DC (plus the aliases), and MOBILE / STATIC_RESIDENTIALCase-insensitive. Any other value fails with VALIDATION_ERROR.
created_after, created_beforeISO 8601 timestampsBoth inclusive, on created_at.
sortcreated_at | totalDefaults to created_at.
orderasc | descDefaults to desc.
limit, cursorsee Pagination

Every parameter here is checked. An unrecognized status, category, sort, or order, and an unparseable date, all fail with VALIDATION_ERROR naming the parameter. Changing sort or order partway through a paginated walk invalidates the cursor you're holding, see Pagination.

Unknown filter values are rejected, not ignored

A status or category outside the sets above returns VALIDATION_ERROR (400) with details: [{ "field": "status", "issue": "invalid", "allowed": [...] }]. ?status=paidd is an error, not every order you've ever placed. Because the filter you sent is always the filter that ran, an empty page means you have no matching orders.

This filter reaches more categories than POST /orders sells. An account can hold MOBILE or STATIC_RESIDENTIAL orders bought from the dashboard, and those appear in the unfiltered list, so you can narrow to them here even though you cannot buy them through the API.

curl "https://dashboard.proxio.net/api/v1/orders?status=PAID&limit=20" \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.get(
    f"{BASE}/orders",
    headers={"Authorization": f"Bearer {API_KEY}"},
    params={"status": "PAID", "limit": 20},
    timeout=15,
)
resp.raise_for_status()
print(resp.json()["data"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/orders?status=PAID&limit=20`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
})
console.log((await res.json()).data)

200 response:

{
  "data": [
    {
      "id": "clord_9f2a",
      "status": "PAID",
      "category": "RESIDENTIAL",
      "is_unlimited": false,
      "quantity_gb": 50,
      "ip_quantity": null,
      "subtotal": "125.00",
      "discount": "12.50",
      "total": "112.50",
      "paid_via": "WALLET",
      "service_id": "clpkg_2a9x",
      "created_at": "2026-07-16T10:00:00.000Z"
    }
  ],
  "meta": { "next_cursor": null, "has_more": false, "request_id": "req_8Ke2jP4mQ" }
}

GET /orders/{id} returns a single order in the same shape, plus a pricing_snapshot with the resolved line items.

Order status

status is one of five values:

StatusMeaning
PENDINGCreated but not yet paid.
PAIDCharged successfully. Provisioning follows.
CANCELEDCancelled before payment.
REFUNDEDPaid, then refunded.
EXPIREDLeft unpaid past its validity.

Quantity fields

An order carries its size in one of two fields, never both:

  • Metered (residential): quantity_gb is the GB purchased, and ip_quantity is null.
  • Unlimited (ISP, datacenter): quantity_gb is null and ip_quantity holds the number of IP slots. Reading quantity_gb on an unlimited order and treating null as zero will silently under-report the order.

Check is_unlimited to know which field to read.

Quote an order

POST /orders/quote prices an order without placing it, same body, same pricing resolvers and coupon evaluation as placing one, so a quote and the purchase that follows it can never disagree. Nothing is written: no order row, no coupon redemption, no wallet movement.

This is a read, not a purchase

Despite the POST verb, this endpoint only requires the read scope, not purchase. It's priced so a pipeline can budget and decide whether to buy using a read-only key, long before anything holding purchase gets involved. No Idempotency-Key needed either, quoting twice is free and has no side effect to deduplicate.

Body is identical to POST /orders: category, quantity_gb, days, ip_quantity, coupon, and the same category values and validation.

curl -X POST https://dashboard.proxio.net/api/v1/orders/quote \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
  -H "Content-Type: application/json" \
  -d '{ "category": "RESIDENTIAL", "quantity_gb": 50, "coupon": "SUMMER10" }'
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.post(
    f"{BASE}/orders/quote",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"category": "RESIDENTIAL", "quantity_gb": 50, "coupon": "SUMMER10"},
    timeout=15,
)
resp.raise_for_status()
quote = resp.json()["data"]
print(quote["total"], quote["wallet"]["covers_total"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/orders/quote`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ category: "RESIDENTIAL", quantity_gb: 50, coupon: "SUMMER10" }),
})
const quote = (await res.json()).data
console.log(quote.total, quote.wallet.covers_total)

200 response:

{
  "data": {
    "category": "residential",
    "currency": "USD",
    "subtotal": "125.00",
    "discount": "12.50",
    "total": "112.50",
    "pricing": {
      "kind": "metered",
      "quantity_gb": 50,
      "unit_price_gb": 2.5,
      "tier_percent_applied": 0,
      "ips_per_gb": 0
    },
    "coupon": {
      "code": "SUMMER10",
      "applied": true,
      "name": "Summer promo",
      "percent_off": 10,
      "amount_off": null
    },
    "wallet": { "currency": "USD", "balance": "30.00", "covers_total": false }
  },
  "meta": { "request_id": "req_8Ke2jP4mQ" }
}
  • category on a quote is lowercase and spelled out ("residential", "isp", "datacenter"), unlike everywhere else in this API, including the order category this quote otherwise mirrors, which is always the uppercase canonical form (RESIDENTIAL, ISP, DC). Compare case-insensitively, or normalize it yourself, if you're matching a quote's category against a purchase's.
  • pricing is one of two shapes: kind: "metered" (residential, shown above) or kind: "unlimited" (ISP/datacenter), the same two shapes the purchase itself resolves against. It's context for the price, not something to re-derive total from yourself.
  • coupon is null when you didn't send one. When you did, applied: false means the code didn't qualify, no reason is given: a per-reason answer would let you probe which codes exist and how close they are to their redemption caps.
  • wallet.covers_total is exactly the check the purchase itself makes (it fails with INSUFFICIENT_BALANCE when balance < total), so false here means placing this exact order right now would be refused for want of funds. Top up before you retry it as a purchase.

A quote is a snapshot, not a lock

Nothing about calling this endpoint reserves the price, a coupon's remaining redemptions, or your balance. If you quote and then wait, the purchase that follows re-resolves pricing and re-evaluates the coupon from scratch and can land on a different number.

Place an order

POST /orders. Requires the purchase scope and an Idempotency-Key (without it, the request fails with MISSING_PARAMETER).

Body

FieldTypeNotes
categorystringRESIDENTIAL, ISP, or DC. Case-insensitive, aliases accepted, see Category values.
quantity_gbnumberRequired for residential (per-GB).
daysnumberRequired for ISP/DC. One of the durations advertised by GET /products (allowed_days).
ip_quantitynumberISP/DC only. Default 1.
couponstringOptional discount code.

Category values

Input is normalized onto a canonical enum, so what you send back is not necessarily what you get. Responses always use RESIDENTIAL, ISP, or DC.

Canonical valueAlso accepted on input
RESIDENTIALresidential
ISPisp, isp_rotating
DCdc, dc_rotating, datacenter

Matching is case-insensitive and surrounding whitespace is trimmed, so " Datacenter " resolves to DC. Compare against the canonical value when you read a response, never against the string you sent. Anything outside the table fails with VALIDATION_ERROR, both in a POST /orders body and as a GET /orders filter.

The allowed days values are config-driven, not a fixed list: read them from allowed_days on the matching product. A days value outside that set fails with VALIDATION_ERROR (400) and details: [{ "field": "days", "allowed": [...] }].

curl -X POST https://dashboard.proxio.net/api/v1/orders \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "category": "RESIDENTIAL", "quantity_gb": 50, "coupon": "SUMMER10" }'
import uuid
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.post(
    f"{BASE}/orders",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"category": "RESIDENTIAL", "quantity_gb": 50, "coupon": "SUMMER10"},
    timeout=30,
)
resp.raise_for_status()
print(resp.status_code, resp.json()["data"])  # 201
import { randomUUID } from "node:crypto"

const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/orders`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({ category: "RESIDENTIAL", quantity_gb: 50, coupon: "SUMMER10" }),
})
console.log(res.status, (await res.json()).data) // 201

201 response:

{
  "data": {
    "order": { "id": "clord_9f2a", "status": "PAID", "subtotal": "125.00", "discount": "12.50", "total": "112.50", "paid_via": "WALLET" },
    "service": { "id": "clpkg_2a9x", "category": "RESIDENTIAL", "status": "active" },
    "wallet": { "balance": "30.00" }
  },
  "meta": { "request_id": "req_8Ke2jP4mQ" }
}

Provisioning can trail the charge

On success the order is PAID and provisioning runs. If provisioning is still finishing, service may be null and meta.provisioning is "pending". Poll GET /orders/{id} until its service_id is populated. The charge is never applied twice.

If your balance can't cover the total, the order fails with INSUFFICIENT_BALANCE (402), top up and retry with the same idempotency key.

A failed order leaves a PENDING row behind

The order row is written before the wallet is charged, so an order that fails at the charge, most commonly on INSUFFICIENT_BALANCE, stays in your history with status: "PENDING". Nothing was charged and nothing was provisioned, but the row is real: it appears in GET /orders and in ?status=PENDING.

Each failed attempt adds another one, so a retry loop against an underfunded wallet accumulates orphan pending orders. Filter them out when you reconcile, treat only PAID as a real purchase, and top up before retrying instead of hammering the endpoint.

Renew or top up a service

POST /services/{id}/renew tops up or extends an existing service. Same requirements as placing an order: purchase scope and a required Idempotency-Key. Send one of two body shapes, depending on what you're doing and what kind of package it is:

  • Top up a metered package: { "type": "topup", "quantity_gb": 25 }. Adds traffic to a metered package (residential and other metered categories) right away. Topping up an unlimited package returns UNSUPPORTED_OPERATION.
  • Extend an unlimited ISP/DC package: { "type": "extend", "days": 30 }. Priced at the per-day rate times the number of active IP slots, so a 5-IP package is billed for all 5. The expiry moves forward by exactly days, immediately.

Any other package kind, including Residential, returns UNSUPPORTED_OPERATION for type: "extend". Residential packages aren't manually extendable: they renew via auto-renewal instead, which charges your wallet the current price for another cycle automatically. Use topup to add data to a residential package right now.

The days value for an unlimited ISP/DC extend is config-driven: it must be one of the extension durations the service's category advertises. An unknown value fails with VALIDATION_ERROR (400) and details: [{ "field": "days", "allowed": [...] }].

curl -X POST https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/renew \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "type": "topup", "quantity_gb": 25 }'
import uuid
import requests

BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

resp = requests.post(
    f"{BASE}/services/clpkg_2a9x/renew",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"type": "topup", "quantity_gb": 25},
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["data"])
import { randomUUID } from "node:crypto"

const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"

const res = await fetch(`${BASE}/services/clpkg_2a9x/renew`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({ type: "topup", quantity_gb: 25 }),
})
console.log((await res.json()).data)

201 response (topup on a metered package):

{
  "data": {
    "order": { "id": "clord_7c1d", "status": "PAID", "total": "62.50" },
    "service": {
      "id": "clpkg_2a9x",
      "remaining": { "bytes": "75000000000", "bytes_num": 75000000000, "gigabytes": 75 },
      "expires_at": "2026-09-01T00:00:00.000Z"
    }
  },
  "meta": { "request_id": "req_8Ke2jP4mQ" }
}

201 response (extend on an unlimited ISP/DC package). The shape is identical, but remaining is null, because an unlimited package has no bandwidth counter to report. Only expires_at moves:

{
  "data": {
    "order": { "id": "clord_7c1d", "status": "PAID", "total": "86.40" },
    "service": {
      "id": "clpkg_5d3m",
      "remaining": null,
      "expires_at": "2026-10-01T00:00:00.000Z"
    }
  },
  "meta": { "request_id": "req_8Ke2jP4mQ" }
}

Don't assume remaining is an object: check for null before reading remaining.bytes.

On this page