ProxioDocs
API Reference

Usage

Read bandwidth usage for a Proxio service. GET /services/{id}/usage returns a summary (limit, used, remaining, today, success rate) and GET /services/{id}/usage/series returns a zero-filled time series with close-reason breakdowns. Explains the byte-quantity object.

Two endpoints report bandwidth for a service: a summary for headline numbers and a series for charts. Both express byte amounts as quantity objects, so start with how those work.

Scope: read

Byte quantities

Bandwidth is metered in bytes, and exact byte counts can exceed what a JSON number holds precisely. To avoid precision loss, every byte-valued field is a quantity object with three views of the same value:

"remaining": {
  "bytes": "10485760000",
  "bytes_num": 10485760000,
  "gigabytes": 10.4858
}
FieldTypeNotes
bytesstringExact integer bytes, as a string. Always present, always authoritative.
bytes_numnumber | nullThe same value as a JSON number, present only when it fits safely (up to 9007199254740991); otherwise null.
gigabytesnumberConvenience float (bytes / 1e9), rounded to 4 decimals.

Prefer the bytes string

Read bytes (the string) when you need exactness. Use bytes_num only for quick math where you've confirmed it isn't null, and treat gigabytes as a display convenience. For realistic package sizes bytes_num is virtually always populated, but the string is the field to trust.

For unlimited packages, limit and remaining are null rather than a quantity object.

Usage summary

GET /services/{id}/usage returns the current headline figures.

curl https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/usage \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
import requests

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

resp = requests.get(
    f"{BASE}/services/{SERVICE_ID}/usage",
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=15,
)
resp.raise_for_status()
data = resp.json()["data"]
print(data["remaining"]["bytes"], data["success_rate_7d"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const SERVICE_ID = "clpkg_2a9x"

const res = await fetch(`${BASE}/services/${SERVICE_ID}/usage`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
})
const { data } = await res.json()
console.log(data.remaining.bytes, data.success_rate_7d)

200 response:

{
  "data": {
    "service_id": "clpkg_2a9x",
    "is_unlimited": false,
    "limit":     { "bytes": "50000000000", "bytes_num": 50000000000, "gigabytes": 50 },
    "used":      { "bytes": "18760000000", "bytes_num": 18760000000, "gigabytes": 18.76 },
    "remaining": { "bytes": "31240000000", "bytes_num": 31240000000, "gigabytes": 31.24 },
    "today":     { "bytes": "820000000",   "bytes_num": 820000000,   "gigabytes": 0.82 },
    "connections_7d": 15234,
    "blocked_7d": 340,
    "success_rate_7d": 0.987
  },
  "meta": { "request_id": "req_8Ke2jP4mQ", "degraded": false }
}

remaining is the authoritative quota counter, not limit minus used: the two are measured independently (see Accounting below), so they can drift apart by a small amount rather than always summing exactly to limit.

blocked_7d counts connections refused on purpose over the last 7 days: your own blocklist rules, the global destination denylist, or your package/credential quota. Never a failure on our end. success_rate_7d excludes those on both sides of the ratio, so enabling a blocklist preset doesn't drag your success rate down for using it (see Connection quality below). It's null when there were no non-blocked attempts in the window, which includes the case where every connection in the window was a deliberate block.

Accounting

Both directions count: usage is metered as bytes_in + bytes_out at the proxy, not just the response body your client sees, so it includes protocol and connection overhead your own byte counter probably doesn't. A few other things are worth knowing before you compare this number to one you're keeping yourself:

  • used and remaining are measured independently, so in the moments right after a burst of traffic they can briefly disagree by a small amount rather than summing exactly to limit. Both settle within a few minutes. remaining is the figure quota enforcement uses, so trust it for "can I still send traffic"; used is the one to trust for "how much have I sent".
  • today can lag by a few minutes. It is aggregated rather than counted live, so traffic from the last few minutes may not appear in it yet. used is not subject to that lag.
  • today is the UTC calendar day, not your local day or a rolling 24 hours. It resets at UTC midnight.
  • GB is decimal SI, bytes / 1e9, the same convention as every other byte quantity in the API. If you're comparing against a tool that reports GiB (bytes / 2^30), your numbers will disagree by about 7% even when the underlying byte count matches exactly.

Why your number differs from ours: the most common causes, in order, are counting only response bytes instead of both directions, reading today before the last few minutes have been aggregated, and a GiB vs. GB unit mismatch. If none of those explain the gap, compare the exact bytes string, not gigabytes, since the latter is rounded for display.

Usage series

GET /services/{id}/usage/series returns time-bucketed usage for charts, zero-filled across the whole range so every bucket is present.

Query parameters

ParameterValuesDefaultNotes
granularityhour | daydayBucket size.
fromISO 8601now - 7 daysStart of the range.
toISO 8601nowEnd of the range.
group_bycountry-Adds a by_country array to the response. See Breaking down by country.

Maximum range is 180 days for day granularity and 7 days for hour.

curl "https://dashboard.proxio.net/api/v1/services/clpkg_2a9x/usage/series?granularity=day&from=2026-07-10T00:00:00Z&to=2026-07-17T00:00:00Z" \
  -H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
import requests

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

resp = requests.get(
    f"{BASE}/services/{SERVICE_ID}/usage/series",
    headers={"Authorization": f"Bearer {API_KEY}"},
    params={
        "granularity": "day",
        "from": "2026-07-10T00:00:00Z",
        "to": "2026-07-17T00:00:00Z",
    },
    timeout=15,
)
resp.raise_for_status()
for point in resp.json()["data"]["points"]:
    print(point["start"], point["bytes"])
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const SERVICE_ID = "clpkg_2a9x"

const query = new URLSearchParams({
  granularity: "day",
  from: "2026-07-10T00:00:00Z",
  to: "2026-07-17T00:00:00Z",
})
const res = await fetch(
  `${BASE}/services/${SERVICE_ID}/usage/series?${query}`,
  { headers: { Authorization: `Bearer ${API_KEY}` } },
)
const { data } = await res.json()
for (const point of data.points) console.log(point.start, point.bytes)

200 response:

{
  "data": {
    "granularity": "day",
    "from": "2026-07-10T00:00:00.000Z",
    "to": "2026-07-17T00:00:00.000Z",
    "points": [
      {
        "start": "2026-07-16T00:00:00.000Z",
        "bytes": "820000000",
        "bytes_num": 820000000,
        "gigabytes": 0.82,
        "connections": 2100
      }
    ],
    "close_reasons": {
      "normal": 14980, "idle_timeout": 190, "error": 41, "denied_quota": 23
    }
  },
  "meta": { "request_id": "req_8Ke2jP4mQ", "degraded": false }
}

Each point covers one bucket starting at start (UTC) and inlines a full byte quantity: bytes, bytes_num, and gigabytes are all present on every point, alongside connections. close_reasons aggregates why connections ended over the range, a useful signal: denied_quota climbing means you're running low, and a rising error count is worth investigating.

Connection quality

close_reasons and the summary's blocked_7d / success_rate_7d are built from the same taxonomy: a connection ends for exactly one reason, and every reason falls into one of two buckets, a deliberate block (we, or a rule you configured, refused it on purpose) or an attempt (we actually tried to carry it). Only attempts count toward success_rate_7d's denominator.

close_reasons keys are a closed, public vocabulary of seven values. Any close reason outside this list is reported as error:

ReasonBucketMeaning
normalAttempt (success)Clean close. Carried the connection with no issue.
idle_timeoutAttempt (success)Established, then went quiet past the idle timeout and we closed it. Counted as a success: the connection was carried, and a timeout firing on schedule isn't a fault, but it's broken out on its own because a sudden spike usually means something upstream is stalling.
errorAttempt (failure)A genuine failure, including any reason not listed here.
denied_quotaDeliberate blockThe package or credential was out of quota when the connection was accepted, or ran out mid-flight.
target_deniedDeliberate blockOur global destination denylist refused it. See Blocked Destinations.
customer_ruleDeliberate blockYour own blocklist preset or host rule refused it.
connection_limitDeliberate blockYour own concurrency cap was exceeded.

idle_timeout counts as a success

This trips people up when they add up the numbers themselves: idle_timeout sits in the attempts denominator and is not subtracted from it, so it never lowers success_rate_7d. It's the overwhelmingly common case of your own idle pooled connection, not a proxy fault.

Blocks are the feature working, not the proxy failing

If you turn on a blocklist preset like ads-trackers, every refused request shows up as customer_rule in close_reasons and counts toward blocked_7d, not toward success_rate_7d's denominator. On a typical page load that can be 20 to 40% of requests. Before this separation existed, those deliberate refusals dragged the success-rate figure down and made the feature look like an outage. Now a rising blocked_7d alongside a healthy success_rate_7d means blocking is working as configured, not that anything is wrong.

Breaking down by country

Add group_by=country and the response gains a by_country array beside points. It is a breakdown of the same range, not of each bucket, so it has no start field:

{
  "data": {
    "granularity": "day",
    "from": "2026-07-10T00:00:00.000Z",
    "to": "2026-07-17T00:00:00.000Z",
    "points": [],
    "close_reasons": {},
    "by_country": [
      {
        "country": "us",
        "bytes": "610000000",
        "bytes_num": 610000000,
        "gigabytes": 0.61,
        "connections": 1580
      },
      {
        "country": "unknown",
        "bytes": "12000000",
        "bytes_num": 12000000,
        "gigabytes": 0.012,
        "connections": 44
      }
    ]
  },
  "meta": { "request_id": "req_8Ke2jP4mQ", "degraded": false }
}

Entries are ordered by traffic, heaviest first, and each carries the same byte quantity fields as a point. Traffic whose country couldn't be resolved is grouped under "unknown". The field is present only when you pass group_by=country; without it, by_country is absent from data rather than empty. points and close_reasons are returned either way.

Degraded reads

If detailed usage reporting is briefly unavailable, these endpoints degrade rather than fail: they return the best available figures and set meta.degraded: true. Treat used and today as approximate whenever degraded is true; remaining stays authoritative.

On this page