Skip to content
Sandbox open: magic test cards, hosted checkout and signed webhooks · 55 payment methods in the catalogue, cards to crypto · Built and hosted in the EEA · Read the API reference at /developers

Sandbox open: magic test cards, hosted checkout and signed webhooks

Documentation

API reference

Idempotency

How to make a POST safe to retry: the key, the 24-hour replay window, what counts as the same request, and what to do after a timeout.

Updated

Send Idempotency-Key on every POST. Without it, a request that times out leaves you with no way to know whether the payment exists, and the only options are to charge twice or not at all.

curl https://cleared-pay.com/api/v1/payments \
  -H "Authorization: Bearer $CLEARED_SECRET_KEY" \
  -H "Idempotency-Key: 4f0a2c6e-9b31-4c0d-9a7e-1f2b3c4d5e6f" \
  -H "Content-Type: application/json" \
  -d '{"amount":2000,"currency":"eur","reference":"ORDER-10432","payment_method":{"type":"card","token":"tok_01JBXQ2M4K7T9V3ZC8HR5N6PWD"}}'

The rules

You sendYou get
A new keyThe request runs. Its response is stored for 24 hours.
The same key, the same request, within 24 hoursThe stored response, byte for byte, with Idempotent-Replayed: true.
The same key, a different request409 idempotency_error, code idempotency_key_conflict. Nothing runs.
The same key, while the first is still in flightYour request waits up to 5 seconds for the first one, then answers with its result. If it is still running, 409 idempotency_key_in_use with Retry-After: 1.
The same key after 24 hoursA fresh request. The key is free again.

A replay returns whatever the original returned — including a 402 decline, a 422 failure and a 500. That is the point: the retry answers the question "what happened", not "try again".

Keys are at most 255 characters and scoped to your merchant and environment. A UUID per logical operation is the right choice; a timestamp or a counter is not, because it changes when your process restarts mid-retry.

What counts as the same request

The comparison is over the HTTP method, the path, and a canonical form of the JSON body in which object keys are sorted and undefined values are dropped. So key order and whitespace do not matter, and a changed amount does.

The path is part of it, so the same key on POST /v1/payments and on POST /v1/payments/:id/refunds is a conflict, not two operations. Use one key per operation.

Which endpoints

Every POST honours the header. Four deliberately do not, because they are inherently repeatable and a stored replay would hide the result you asked for:

  • POST /v1/sandbox/tokens — a token is single-use by design
  • POST /v1/sandbox/simulate-webhook — you want the new delivery attempt, not the old one
  • POST /v1/events/:id/resend — same reason
  • POST /v1/sandbox/advance/:paymentId — advancing twice is not meaningful

GET and DELETE are idempotent by nature and ignore the header.

Retrying correctly

  1. Generate the key before the first attempt and keep it with the operation, not with the attempt.
  2. Retry on a timeout, a connection error, a 5xx or a 429 — with the same key, and with backoff.
  3. Do not retry a 400, 402, 404 or 422. The answer will not change.
  4. Never regenerate the key on retry. A new key is a new payment.
const key = crypto.randomUUID();           // once per order, stored with the order

async function createPayment(body: unknown) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch("https://cleared-pay.com/api/v1/payments", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.CLEARED_SECRET_KEY}`,
        "Idempotency-Key": key,           // the same key on every attempt
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (res.status < 500 && res.status !== 429) return res;     // final answer, decline included
    const retryAfter = Number(res.headers.get("retry-after") ?? 0);
    await new Promise((r) => setTimeout(r, Math.max(retryAfter * 1000, 2 ** attempt * 250)));
  }
  throw new Error("Cleared-pay did not answer; the Idempotency-Key is safe to reuse.");
}

If you have lost the key and need to know whether a payment happened, look it up by your own reference:

curl -G https://cleared-pay.com/api/v1/payments \
  -H "Authorization: Bearer $CLEARED_SECRET_KEY" \
  -d "reference=ORDER-10432"

reference is yours and is not enforced unique, so this is a recovery path, not a substitute for the key.

What idempotency does not cover

It makes a request safe to repeat. It does not make an operation safe to repeat with a new key: two payments created with two keys are two payments, whatever the reference says. Deduplicating at your own boundary — one key per order, stored before the first attempt — is the part only you can do.