ProxioDocs
API Reference

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

FieldTypeNotes
namestringRequired, 1 to 120 characters.
scopesstring[]Required, at least one of read, write, purchase.
expires_atstring | nullISO 8601 UTC timestamp (Z suffix). Omit or send null to inherit the calling key's own expiry.
allowed_ipsstring[] | nullUp to 50 IPv4/IPv6 addresses or CIDR ranges. Omit or send null to inherit the calling key's own allowlist.
rate_limit_per_minnumber | nullOmit 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 once
import { 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 once

201 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)  # 204
const 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) // 204

On this page