API Keys
Manage Proxio API keys programmatically with GET/POST /api-keys and DELETE /api-keys/{id}. A key can only mint a key that is weaker than or equal to itself, the plaintext token is returned exactly once, and expiry, rate limit, and IP allowlist all clamp to the calling key's own values.
Beyond the dashboard's Settings → API keys page, keys can be listed, minted, and revoked from the API itself, so an automated pipeline can rotate its own credentials without a human in the loop. See Authentication for the key format, scopes, and how a request is authenticated; this page covers the management endpoints.
Scopes: read to list, write to create and revoke.
A key can only mint a weaker-or-equal key
The governing rule on every field below: a key may never mint a key more powerful than itself. Scopes must be a subset of the calling key's own scopes, and the per-minute limit, expiry, and IP allowlist each clamp to the calling key's own value. Possession of one key can never be traded up into a stronger one.
List keys
GET /api-keys returns your own keys, cursor-paginated,
newest first. There's no sort, order, or date-range filter on this list.
Revoked and expired keys stay listed, with their revoked_at / expires_at
timestamps, so this doubles as an audit of everything that was ever issued.
The plaintext token is never included here, only key_prefix.
curl https://dashboard.proxio.net/api/v1/api-keys \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.get(
f"{BASE}/api-keys",
headers={"Authorization": f"Bearer {API_KEY}"},
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}/api-keys`, {
headers: { Authorization: `Bearer ${API_KEY}` },
})
console.log((await res.json()).data)200 response:
{
"data": [
{
"id": "clkey_4p9x",
"name": "ci-pipeline",
"key_prefix": "pxo_9fJ2kQ7x",
"scopes": ["read", "write"],
"allowed_ips": [],
"rate_limit_per_min": null,
"created_at": "2026-07-17T09:30:00.000Z",
"last_used_at": "2026-08-01T14:02:11.000Z",
"expires_at": null,
"revoked_at": null
}
],
"meta": { "next_cursor": null, "has_more": false, "request_id": "req_8Ke2jP4mQ" }
}rate_limit_per_min: null means the key runs at the platform
default (120/min) rather than a lowered override.
Create a key
POST /api-keys mints a new key. The plaintext token is returned exactly
once, in this response, and is never recoverable afterward, only its SHA-256
hash is stored, exactly like a key created in the dashboard.
Body
| Field | Type | Notes |
|---|---|---|
name | string | Required, 1 to 120 characters. |
scopes | string[] | Required, at least one of read, write, purchase. |
expires_at | string | null | ISO 8601 UTC timestamp (Z suffix). Omit or send null to inherit the calling key's own expiry. |
allowed_ips | string[] | null | Up to 50 IPv4/IPv6 addresses or CIDR ranges. Omit or send null to inherit the calling key's own allowlist. |
rate_limit_per_min | number | null | Omit to inherit the calling key's effective limit (see below). |
Scopes must be a subset
scopes can only contain scopes the calling key itself holds. Asking for
one it doesn't have fails with
INSUFFICIENT_SCOPE (403) and
details naming each missing scope:
{ "error": { "code": "INSUFFICIENT_SCOPE", "details": [{ "required": "purchase" }], "...": "..." } }A read-only key can never bootstrap itself a write or purchase key this
way.
Rate limit inherits, and never exceeds the caller's own
The ceiling for a minted key is the lower of two numbers: the calling
key's own effective limit, and the platform default
(120/min). Sending rate_limit_per_min above that ceiling fails with
VALIDATION_ERROR and
details: [{ "field": "rate_limit_per_min", "issue": "exceeds_ceiling", "max": ... }].
Omit the field and the new key inherits the calling key's own limit, but
only when that limit is already below the default. A calling key running at
the plain default produces a new key at the default too, not a stored 120.
Raising a key above the default remains a Proxio support action, exactly as in
the dashboard.
Expiry never outlives the calling key
If the calling key itself carries an expires_at, a minted key can't be given
a later one: an expires_at past the calling key's own fails with
VALIDATION_ERROR and
details: [{ "field": "expires_at", "issue": "exceeds_calling_key_expiry", "max": "..." }].
Omitting the field inherits the calling key's expiry exactly, including "never
expires" if that's what the calling key has. expires_at must also be in the
future; a past timestamp fails with issue: "not_in_future".
IP allowlist can only narrow
If the calling key itself carries an IP allowlist, every entry in the new
key's allowed_ips must fall inside one of the calling key's own ranges,
a /28 narrows a covering /24, but a /24 cannot widen a /28. An entry
outside the calling key's allowlist fails with
VALIDATION_ERROR and
details: [{ "field": "allowed_ips", "issue": "outside_calling_key_allowlist", "value": "..." }]
per offending entry. Omit the field to inherit the calling key's allowlist
exactly. A calling key with no allowlist of its own places no such
constraint, the new key can carry any allowlist, or none.
curl -X POST https://dashboard.proxio.net/api/v1/api-keys \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "name": "ci-pipeline", "scopes": ["read", "write"] }'import uuid
import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.post(
f"{BASE}/api-keys",
headers={
"Authorization": f"Bearer {API_KEY}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"name": "ci-pipeline", "scopes": ["read", "write"]},
timeout=15,
)
resp.raise_for_status()
created = resp.json()["data"]
print(created["secret"]) # pxo_..., shown onceimport { randomUUID } from "node:crypto"
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const res = await fetch(`${BASE}/api-keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({ name: "ci-pipeline", scopes: ["read", "write"] }),
})
const created = (await res.json()).data
console.log(created.secret) // pxo_..., shown once201 response:
{
"data": {
"id": "clkey_4p9x",
"name": "ci-pipeline",
"key_prefix": "pxo_9fJ2kQ7x",
"scopes": ["read", "write"],
"allowed_ips": [],
"rate_limit_per_min": null,
"created_at": "2026-07-17T09:30:00.000Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null,
"secret": "pxo_3mR8vT1yN6qX9wZ2lC4kS7pJ0aH5eB8dF1gK4nQr"
},
"meta": { "request_id": "req_8Ke2jP4mQ" }
}The token comes back null on a replay
secret follows the same one-time contract as the webhook signing secret
and a credential's password: it rides only the original response. An
idempotent replay of this create, the
same Idempotency-Key sent again, returns the identical key resource with
secret: null instead of handing the token out a second time. If you lose
it before saving it, revoke the key and mint a replacement.
Revoke a key
DELETE /api-keys/{id} returns 204 No Content. Revocation is a soft delete:
the key stays listed with its revoked_at timestamp for audit, and any
further call made with it fails with
REVOKED_API_KEY. A key may revoke
itself, it simply stops authenticating on the next request. Calling this
again on an already-revoked key is safe: it keeps the original revoked_at
and still returns 204. An id that isn't yours, or never existed, returns
NOT_FOUND rather than a 403, so the API never
confirms whether a given id exists.
curl -X DELETE https://dashboard.proxio.net/api/v1/api-keys/clkey_4p9x \
-H "Authorization: Bearer pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
resp = requests.delete(
f"{BASE}/api-keys/clkey_4p9x",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
print(resp.status_code) # 204const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
const res = await fetch(`${BASE}/api-keys/clkey_4p9x`, {
method: "DELETE",
headers: { Authorization: `Bearer ${API_KEY}` },
})
console.log(res.status) // 204Related pages
Account
Read your Proxio profile, wallet balance, and service totals in one call with GET /account. Includes cURL, Python, and Node.js examples.
Products
Read the Proxio catalog and live pricing with GET /products, including per-GB residential rates with volume tiers and per-IP-per-day ISP and datacenter pricing with duration discounts. ETag / If-None-Match give you a cheap 304 when pricing hasn't changed.

