ProxioDocs
API Reference

Errors

The complete Proxio API error catalog. Every error returns a stable UPPER_SNAKE code, an HTTP status, a request_id, and a doc_url that deep-links to the matching section here. Includes retry guidance per code family.

Every error the API itself raises uses one envelope. There is no header-encoded error, no bare string, and no HTML page, always this JSON object under an error key, calling a real path with a verb it doesn't support is no exception: it answers METHOD_NOT_ALLOWED in the same envelope, with an Allow header naming the verbs that path does support.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "quantity_gb must be an integer >= 1.",
    "doc_url": "https://docs.proxio.net/docs/api/errors#validation_error",
    "request_id": "req_8Ke2jP4mQ",
    "details": [{ "field": "quantity_gb", "issue": "too_small", "min": 1 }]
  }
}
  • code is a stable, machine-readable UPPER_SNAKE value from the closed catalog below. Branch on this, never on the message.
  • message is a human-readable English string, safe to log. It may change wording between releases.
  • doc_url deep-links to this page. The anchor is always the lowercased code, so INVALID_CURSOR links to #invalid_cursor.
  • request_id mirrors the X-Request-Id response header. Quote it when you contact support, it pins the exact request in our logs.
  • details is an optional array with structured context (for example the offending fields on a validation error). It is absent when not applicable.

Adding a new code in a later release is a backward-compatible change, so treat any unrecognized code as a generic failure of its HTTP status class.

Catalog

The Retry column tells you whether resending the same request can succeed: retry codes are transient, non-retry codes need you to change something first.

CodeHTTPRetryMeaning
UNAUTHENTICATED401NoNo credentials presented.
INVALID_API_KEY401NoMalformed key or no matching key.
EXPIRED_API_KEY401NoKey is past its expires_at.
REVOKED_API_KEY401NoKey was revoked or deactivated.
INSUFFICIENT_SCOPE403NoValid key lacks the required scope.
IP_NOT_ALLOWED403NoCaller IP is outside the key's allowlist.
ACCOUNT_SUSPENDED403NoThe owning account is suspended.
FORBIDDEN403NoGeneric authorization failure. Reserved (not currently returned by any endpoint).
NOT_FOUND404NoResource doesn't exist, or isn't owned by the caller.
METHOD_NOT_ALLOWED405NoHTTP verb not supported on this path. Allow header (and details[0].allow) names the verbs that are.
VALIDATION_ERROR400NoBody or query failed schema validation. details[] present.
MALFORMED_JSON400NoRequest body isn't valid JSON.
MISSING_PARAMETER400NoA required query, path, or header parameter is absent.
INVALID_CURSOR400NoPagination cursor is unparseable or tampered.
UNSUPPORTED_OPERATION400NoOperation is invalid for this resource's state.
INVALID_IP400NoWhitelist IP is not a public, routable address, or is malformed.
CONFLICT409NoMostly an idempotency dead end: the key can't be reused. Retry with a new key.
DUPLICATE_RESOURCE409NoA unique constraint was violated.
IP_ALREADY_BOUND409NoWhitelist IP is already on one of your own credentials. details:[{credential_id}].
IP_UNAVAILABLE409NoWhitelist IP is unavailable. Choose a different IP.
LIMIT_REACHED409NoA per-resource cap was hit. details:[{limit}].
IDEMPOTENCY_KEY_REUSED409NoSame Idempotency-Key, different request body.
IDEMPOTENCY_IN_PROGRESS409YesA request with this key is still being processed.
INSUFFICIENT_BALANCE402NoWallet can't cover the order or renewal.
QUOTA_EXCEEDED402NoA quota-gated write was blocked. Reserved (not currently returned by any endpoint).
RATE_LIMITED429YesPer-key rate limit exceeded. Retry-After is set.
UPSTREAM_ERROR502YesAn upstream network dependency failed.
SERVICE_UNAVAILABLE503YesA dependency is temporarily down.
INTERNAL_ERROR500YesUnhandled server error. Quote the request_id.

Authentication (401)

These mean the key itself was rejected. Fix the credential; retrying the same request won't help. See Authentication.

UNAUTHENTICATED

No Authorization header was sent. Add Authorization: Bearer pxo_….

INVALID_API_KEY

The header wasn't a well-formed Bearer pxo_… value, or no key matches the hash. A malformed key and an unknown key deliberately return the same code (the message text differs slightly between the two, but neither one says which case you hit), so the API never confirms whether a given prefix exists. Re-copy the key.

EXPIRED_API_KEY

The key is past its expires_at. Create a new key (or one without an expiry) and swap it in.

REVOKED_API_KEY

The key was revoked or deactivated. Revocation is permanent; mint a replacement.

Authorization (403)

The key is valid but not permitted to do this. Change the key or the caller, not the request payload.

INSUFFICIENT_SCOPE

The key doesn't carry the scope the endpoint requires. details names the missing scope, for example [{ "required": "purchase" }]. Create a key with the right scope.

IP_NOT_ALLOWED

The caller's IP is outside the key's IP allowlist. Call from an allowed IP, or update the allowlist on the key.

ACCOUNT_SUSPENDED

The account that owns the key is suspended. Contact support, no key change will lift it.

FORBIDDEN

A generic authorization failure that isn't covered by a more specific 403 code. Reserved: it is defined in the catalog but not currently returned by any endpoint.

Not found (404)

NOT_FOUND

The resource doesn't exist, or it exists but isn't owned by your account. Ownership failures return 404, never 403, so the API never confirms the existence of resources you can't see. Check the id.

Method (405)

METHOD_NOT_ALLOWED

The path exists but doesn't support this HTTP verb, DELETE /locations for example, the geo catalog has no DELETE. It's the ordinary error envelope like every other code on this page, plus an Allow header naming the verbs that path actually does support:

HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, OPTIONS
{
  "error": {
    "code": "METHOD_NOT_ALLOWED",
    "message": "The DELETE method is not supported on this endpoint.",
    "doc_url": "https://docs.proxio.net/docs/api/errors#method_not_allowed",
    "request_id": "req_8Ke2jP4mQ",
    "details": [{ "method": "DELETE", "allow": "GET, HEAD, OPTIONS" }]
  }
}

details[0].allow is the same string as the Allow header, in the body too in case your client doesn't surface response headers as conveniently as it surfaces JSON. HEAD is listed automatically whenever GET is supported, and OPTIONS always is. Check the method against the endpoint's reference page rather than guessing from this response, Allow tells you what's supported on this path, not what the verb you meant to use is called elsewhere in the API. This check runs before authentication, so a wrong-verb call answers 405 even with no Authorization header, or an invalid one, it never spends a rate-limit unit either.

Validation (400)

The request was understood but rejected. Correct the request and resend.

VALIDATION_ERROR

The body or query failed schema validation. details[] lists each offending field with its issue (and constraints like min). Fix the named fields.

MALFORMED_JSON

The request body isn't valid JSON. Check for trailing commas, unquoted keys, or a wrong Content-Type.

MISSING_PARAMETER

A required query, path, or header parameter is absent. For a money endpoint called without an idempotency key, details is [{ "header": "Idempotency-Key" }]. Supply the missing parameter.

INVALID_CURSOR

The pagination cursor couldn't be decoded, it was truncated or altered. Never build cursors yourself; only pass back the exact meta.next_cursor you received. See Pagination.

UNSUPPORTED_OPERATION

The operation is invalid for this resource's current state, for example creating a credential on a static IP (ISP/datacenter) service, or topping up an unlimited package. The message explains the specific reason.

INVALID_IP

Only a public, routable address can be whitelisted. details[0].reason is one of two values:

ReasonMeaning
INVALID_IPThe value isn't a parseable IP address.
PRIVATE_OR_RESERVEDThe address parses but isn't eligible: private, reserved, loopback, link-local, CGNAT, or otherwise not usable for whitelisting.

The API deliberately does not distinguish further, so don't build logic on why a specific address was refused. Submit an address you control from the public internet. See Whitelist.

Conflict (409)

CONFLICT

In practice this is the idempotency layer telling you a key is spent and cannot be reused. It arrives in two situations, and the message distinguishes them:

SituationWhat happened
A previous request with this key did not completeThe original attempt was abandoned and stayed unfinished for 300 seconds. A committed side effect can't be ruled out, so it is never re-run.
A previous request with this key failed after it startedAn earlier attempt began executing and then errored in a way that may have left a partial effect.

Both mean the same thing for your code: the operation is not retryable under this key. Generate a new Idempotency-Key and send the request again. Reusing the same key will keep returning CONFLICT until the 24-hour record expires.

Before you retry, check whether the original operation actually landed. For an order, list GET /orders and look for a PAID order matching what you intended; retrying blindly with a new key is what turns one purchase into two.

IDEMPOTENCY_IN_PROGRESS becomes CONFLICT

A key held by a still-running request returns the retryable IDEMPOTENCY_IN_PROGRESS. If that request never finishes, the same key flips to CONFLICT once 300 seconds pass, and that answer is permanent. So a poll loop on IDEMPOTENCY_IN_PROGRESS must handle CONFLICT as its terminal case: stop retrying that key, reconcile, and start over with a new one.

CONFLICT is also reserved as the generic 409 for any future write conflict not covered by a more specific code, so branch on the code and read the message.

DUPLICATE_RESOURCE

A uniqueness constraint was violated, for example a webhook URL you've already registered. Reuse the existing resource or choose a different value.

IP_ALREADY_BOUND

The whitelist IP is already bound to one of your own credentials. details carries [{ "credential_id": ... }] naming that credential. Remove the existing binding first, or bind a different IP.

IP_UNAVAILABLE

The IP can't be whitelisted. The API gives no reason by design, and the response says nothing about who, if anyone, holds it. Choose a different IP. See Whitelist.

LIMIT_REACHED

A per-resource cap was hit: 20 credentials per service, 50 whitelist entries per credential, or 20 webhooks per account. details carries the limit. Delete something, or use a different parent resource.

IDEMPOTENCY_KEY_REUSED

You reused an Idempotency-Key with a different request body. Keys are bound to their first request for 24 hours. Use a fresh key for a genuinely new request. See Idempotency.

IDEMPOTENCY_IN_PROGRESS

A previous request with this idempotency key is still being processed. This is retryable: wait briefly and retry with the same key to get the stored result. It is not retryable forever, though. If the original request never completes, the key turns into a permanent CONFLICT after 300 seconds, so cap your poll loop and handle that outcome.

Payment (402)

INSUFFICIENT_BALANCE

Your wallet can't cover the order or renewal total. Top up and retry. See Orders.

QUOTA_EXCEEDED

A quota-gated write was blocked because a quota is exhausted. Raise or reset the relevant quota, then retry. Reserved: it is defined in the catalog but not currently returned by any endpoint.

Rate limit (429)

RATE_LIMITED

You exceeded the per-key rate limit. The response carries Retry-After (seconds) and the X-RateLimit-* headers. Retryable: back off for Retry-After seconds and resend. See Rate Limits.

Server (5xx)

These are transient by nature. Retry with exponential backoff and jitter.

UPSTREAM_ERROR

An upstream network dependency failed. Retryable, usually a fresh attempt succeeds.

SERVICE_UNAVAILABLE

A dependency the endpoint needs is temporarily down and it couldn't degrade. Retryable after a short wait.

INTERNAL_ERROR

An unhandled server error. Retryable with backoff; if it persists, contact support and quote the request_id from the body.

On this page