Skip to content
Dashboard

Warning: Provisional contract proposal

These endpoints are not operational and no SDK is published. Examples use test identifiers only and nothing is sent anywhere.

Reliability

Idempotency

Retry POST requests safely. Send an Idempotency-Key header and KrokoPay performs the operation once, answering every repeat with the original response.

Networks fail in the worst place: after KrokoPay created the charge, but before your server read the response. Without protection, the natural fix, retrying, creates a second charge. An idempotency key tells KrokoPay that several requests are the same operation, so it runs once no matter how many times you send it.

The Idempotency-Key header

Add Idempotency-Key to any POST request. The first request with a given key is processed normally; later requests with the same key and the same body get the stored result.

curl -X POST https://api.krokopay.app/v1/charges \  -H "Authorization: Bearer kp_test_sk_4f9a2c7e1b8d4a6f9c3e5d7b" \  -H "Idempotency-Key: 6f1c2b8e-3d4a-4e5f-9a7b-1c2d3e4f5a6b" \  -H "Content-Type: application/json" \  -d '{    "amount": 49700,    "currency": "BRL",    "description": "Curso Identidade Visual do Zero",    "customer_id": "cus_8Hq2LmZr4TxN1a",    "methods": [      "pix",      "card"    ]  }'
MethodIdempotency-Key
POSTRecommended on every request that creates or changes something: charges, refunds, customers, payment links, subscriptions, pause and resume.
GETNot needed. Reading has no side effects and can always be retried.
DELETENot needed. Deleting the same object twice leaves it in the same state.

Generating keys

  • Use a UUID v4 (crypto.randomUUID()), or a value derived from your own record that is unique per operation, such as order_1042 or refund_1042_1.
  • Generate the key once per operation and store it with the record. Every retry of that operation must reuse it.
  • Use a new key for a new operation: a second refund of the same charge is a different operation and needs its own key.
  • Never reuse a key across unrelated requests, and never put personal data (emails, documents) in it.
  • The maximum key length is to be defined. A UUID (36 characters) is a safe choice.
Generate and store a keyTypeScript
// Generate the key once per operation and store it with the order,// so every retry of that operation reuses it.const idempotencyKey = crypto.randomUUID(); // e.g. "6f1c2b8e-3d4a-4e5f-9a7b-1c2d3e4f5a6b"await db.orders.update(order.id, { idempotencyKey });

What happens on replay

  1. Step 1: First request

    Processed normally. The status code and body are stored with the key.

  2. Step 2: Same key, same body

    Not processed again. You receive the stored status and body, identical to the first response, even if that response was an error such as a validation 422.

  3. Step 3: Same key, different body

    Rejected. See Conflicts.

Success: Replays are safe to show

Because the response is identical, your code does not need a special path for replays: treat the replayed charge exactly as if it had just been created.

As proposed, keys are scoped to your account and to the environment of the API key used, so test requests never collide with production ones. What happens when a second request arrives while the first is still being processed is to be defined; until then, avoid sending the same key concurrently.

Conflicts

Reusing a key with a different body (another amount, another customer) usually means a bug: two operations share a key. KrokoPay refuses the request instead of guessing which one you meant.

curl -X POST https://api.krokopay.app/v1/charges \  -H "Authorization: Bearer kp_test_sk_4f9a2c7e1b8d4a6f9c3e5d7b" \  -H "Idempotency-Key: 6f1c2b8e-3d4a-4e5f-9a7b-1c2d3e4f5a6b" \  -H "Content-Type: application/json" \  -d '{    "amount": 59700,    "currency": "BRL",    "description": "Curso Identidade Visual do Zero",    "customer_id": "cus_8Hq2LmZr4TxN1a",    "methods": [      "pix",      "card"    ]  }'
Proposed error response (409)HTTP
HTTP/1.1 409 ConflictContent-Type: application/json​{  "error": {    "type": "idempotency_error",    "code": "idempotency_key_reused",    "message": "This Idempotency-Key was already used with a different request body."  }}

Information: Proposed

The 409 status and the idempotency_key_reused code are a proposal and may change. The general error format is described in Errors.

How long keys are kept

Stored results are kept for a limited time, after which a key can be used again as if new. That retention period is to be defined. Do not rely on old keys to deduplicate: retry promptly, and for long-running workflows check the state of your own record first (for example, whether the order already has a charge id).

A retry loop with fetch

Retry only when the outcome is unknown or temporary: network errors, timeouts and 5xx responses. A 4xx means the request itself must change, so retrying it unchanged returns the same answer.

Retry with the same keyTypeScript
const API_URL = "https://api.krokopay.app/v1";const SECRET_KEY = process.env.KROKOPAY_SECRET_KEY ?? ""; // kp_test_sk_…​const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));​/** * POSTs with the same Idempotency-Key on every attempt. Retries network * failures and 5xx answers only; any 4xx is returned to the caller as is. */export async function postWithRetry(  path: string,  body: Record<string, unknown>,  idempotencyKey: string,  maxAttempts = 3,): Promise<Response> {  for (let attempt = 1; ; attempt++) {    try {      const response = await fetch(`${API_URL}${path}`, {        method: "POST",        headers: {          Authorization: `Bearer ${SECRET_KEY}`,          "Idempotency-Key": idempotencyKey,          "Content-Type": "application/json",        },        body: JSON.stringify(body), // identical on every attempt      });      if (response.status < 500 || attempt >= maxAttempts) return response;    } catch (error) {      // Timeout or dropped connection: the charge may or may not exist yet.      if (attempt >= maxAttempts) throw error;    }    // Exponential backoff with jitter before the next attempt.    await sleep(2 ** attempt * 250 + Math.random() * 250);  }}​// Usage: the key comes from the order, not from the attempt.const response = await postWithRetry(  "/charges",  { amount: 49700, currency: "BRL", description: "Order 1042", methods: ["pix", "card"] },  order.idempotencyKey,);const charge = await response.json();
ResultRetry?
Network error or timeoutYes, with the same key.
5xxYes, with the same key and a growing delay.
409 conflictNo. Fix the key or the body.
Other 4xx (401, 404, 422)No. Fix the request; see Errors.
2xxNo. Done.