Idempotency
Send an Idempotency-Key on Proxio API mutations so retries never double-charge or double-create. Learn the new / replay / conflict / in-progress semantics, which endpoints require the key, the 24-hour TTL, and the Idempotent-Replay header.
Networks drop responses. When a POST times out, you can't tell whether the
server did the work or not. An idempotency key removes the guesswork: retry the
same request with the same key and you get the same result exactly once, no
double charge, no duplicate resource.
The header
Idempotency-Key: <your-unique-string, up to 255 chars>Choose a value that is unique per logical operation, a UUID (v4) per checkout is ideal. Reuse the same key when (and only when) you're retrying that same operation.
Where it applies
Idempotency is opt-in per endpoint, not blanket coverage. This table is the complete list:
| Endpoint | Idempotency-Key |
|---|---|
POST /orders | Required |
POST /services/{id}/renew | Required |
POST /wallet/topups | Required |
| Credential create, rotate-password | Accepted |
| Whitelist add, whitelist batch add | Accepted |
| Session rotate / delete | Accepted |
| Webhook create, rotate-secret, redeliver | Accepted |
POST /api-keys | Accepted |
Three endpoints require the header: two charge your wallet directly,
placing an order and
renewing a service; the third,
opening a wallet top-up, doesn't move
money itself but opens a payment checkout, and the same guarantee applies:
retry with the same key and you get back the same checkout rather than a
second one for the same intent. Calling any of the three without the header
fails with MISSING_PARAMETER (400) and
details: [{ "header": "Idempotency-Key" }].
Other mutations ignore the header
On any mutation not in the table above, an Idempotency-Key is accepted by
the HTTP layer but has no effect: nothing is stored, no replay happens, and a
retry re-executes the operation. That covers credential update and delete,
whitelist removal, webhook update and delete, and the webhook test endpoint.
Sending the header there is harmless, but do not treat it as a retry guard.
None of those operations move money, so a repeat is not costly, but plan for
the repeat rather than assuming it is suppressed.
Semantics
The key is scoped to your account and bound to the exact request (method, path, and body) it first accompanied. Here's how each case resolves:
| Situation | Result |
|---|---|
| New key | The request runs normally. The response is stored against the key. |
| Replay: same key, same request body | The stored response is returned with an Idempotent-Replay: true header. The operation does not run again. |
| Conflict: same key, different request body | IDEMPOTENCY_KEY_REUSED (409). The original operation is untouched. |
| In progress: same key, first request still running | IDEMPOTENCY_IN_PROGRESS (409). Retryable, wait briefly and retry with the same key. |
| Failed: a prior attempt errored mid-flight | CONFLICT (409). Because a partial side effect can't be ruled out, the key is refused: retry with a new Idempotency-Key. |
| Abandoned: a prior attempt never finished, and 300 seconds passed | CONFLICT (409). Same terminal answer, same fix: a new Idempotency-Key. |
A request rejected before it could do anything, a validation failure or
INSUFFICIENT_BALANCE for example,
releases its key. Fix the cause and retry with the same key.
IDEMPOTENCY_IN_PROGRESS has a deadline
IDEMPOTENCY_IN_PROGRESS is retryable, but only while the original request is
genuinely alive. Once it has been unfinished for 300 seconds, the key is
written off and every later attempt with it returns
CONFLICT instead, permanently. Cap your poll
loop and treat CONFLICT as the signal to reconcile
(GET /orders for a purchase) and start again with a
fresh key.
Secrets are not replayed
Responses that carry a secret (the webhook signing secret from
POST /webhooks or POST /webhooks/{id}/rotate-secret, passwords from
credential POST and rotate-password, and the plaintext token from
POST /api-keys) include it only on
the original response. A replay returns the same resource with the secret
field null. A credential password can be re-read with a GET; a lost
webhook signing secret can only be rotated; a lost API key token can't be
recovered at all, revoke the key and mint a replacement.
A replayed response carries the marker header so you can tell a fresh success from a replayed one:
HTTP/1.1 201 Created
Idempotent-Replay: true
X-Request-Id: req_8Ke2jP4mQSame key means same request
An idempotency key is a promise that the request body hasn't changed. If you need to send a genuinely different request, use a fresh key. Reusing a key with a different body is rejected, not silently applied.
Retention
Idempotency records live for 24 hours from creation, then expire. Within that window a retry replays the stored response; after it, the same key is free to be used for a new request. Keep your retry loops well inside 24 hours, and don't recycle a key across unrelated operations.
A safe retry
Generate one key per operation, then reuse it across every retry attempt:
import uuid
import requests
BASE = "https://dashboard.proxio.net/api/v1"
API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
# One key for this logical purchase, reused on every retry.
idem_key = str(uuid.uuid4())
headers = {
"Authorization": f"Bearer {API_KEY}",
"Idempotency-Key": idem_key,
}
body = {"category": "RESIDENTIAL", "quantity_gb": 50}
resp = requests.post(f"{BASE}/orders", headers=headers, json=body, timeout=30)
resp.raise_for_status()
# On a timeout or 5xx, retrying with the SAME idem_key is safe: it never
# double-charges. A completed request replays with Idempotent-Replay: true.
print(resp.headers.get("Idempotent-Replay"), resp.json()["data"])import { randomUUID } from "node:crypto"
const BASE = "https://dashboard.proxio.net/api/v1"
const API_KEY = "pxo_9fJ2kQ7xR4mN8pL1dW6vB3cH5tZ0aYqS7dK2mN9x"
// One key for this logical purchase, reused on every retry.
const idemKey = randomUUID()
const headers = {
Authorization: `Bearer ${API_KEY}`,
"Idempotency-Key": idemKey,
"Content-Type": "application/json",
}
const body = JSON.stringify({ category: "RESIDENTIAL", quantity_gb: 50 })
const res = await fetch(`${BASE}/orders`, { method: "POST", headers, body })
// Retrying with the SAME idemKey after a timeout or 5xx never double-charges.
console.log(res.headers.get("Idempotent-Replay"), (await res.json()).data)Related pages
Pagination
The Proxio API uses cursor pagination everywhere. Pass limit and cursor, read meta.next_cursor, and loop until it's null. Includes a copy-paste pagination loop in Python and Node.js.
Versioning & Stability
The Proxio API v1 stability policy. The v1 surface is additive-only, breaking changes require a new major version and 12 months notice via a Sunset date, an RFC 9745 Deprecation header, and a Link rel=deprecation, and every response carries X-Proxio-Api-Version.

