Idempotency & rate limits
Make Tokeflow API mutations safe to retry with idempotency keys, and design resilient clients that respect per-key rate limits.
Networks fail. Requests time out. Clients retry. On any payment surface, a retry that silently creates a second charge is a production incident.
Tokeflow gives you two primitives to build resilient integrations:
- Idempotency — safely retry create and charge mutations without duplicating work.
- Rate limits — predictable per-key throughput, with clear signals so your client can back off gracefully.
This page covers both, plus a copy-paste retry helper.
Tokeflow is the orchestration layer. It standardizes the API surface, routing, and replay semantics described here; the connected payment providers perform the actual processing and settlement.
Idempotency
Why idempotency matters
Consider a checkout that calls POST /api/v1/transactions. The request reaches Tokeflow, the charge is initiated downstream, but the connection drops before the response gets back to your server. Your client doesn't know whether the charge succeeded. If it retries blindly, you risk charging the customer twice.
An idempotency key removes that ambiguity. You attach a unique key to the mutation. Tokeflow records the outcome against that key. If the same key arrives again — whether from an automatic retry, a double-click, or a queue redelivery — Tokeflow returns the original result instead of performing the operation a second time.
The idempotency key
Provide the key one of two ways. The header takes precedence if both are present.
| Location | Field | Notes |
|---|---|---|
| HTTP header | Idempotency-Key: <unique> | Recommended. Case-insensitive. |
| Request body | "idempotency_key": "<unique>" | Useful when the header is hard to set (e.g. some queue/webhook relays). |
The value is an opaque string you generate. It must be unique per logical operation — that is, the same key should map to exactly one intended action.
:::tip Recommended key construction Derive the key deterministically from the business operation, not randomly. A good pattern combines a stable business identifier with an attempt counter:
order_12345_attempt_1cart-a1b2c3:checkout- A UUID v4 generated once per logical operation and reused across that operation's retries.
The rule: every retry of the same intended action reuses the same key. A genuinely new action (e.g. the customer chooses to pay again after a failure) gets a new key. :::
Do not generate a fresh random key on every HTTP attempt. That defeats the mechanism — each attempt would be treated as a new, distinct charge.
Replay semantics
Idempotency is scoped per merchant and per key. The first request with a given key executes normally; subsequent requests with the same key replay the stored result.
| Scenario | HTTP status | Body |
|---|---|---|
First request with key K | 201 Created | The created resource |
Replay with the same key K | 200 OK | The same body as the first response |
The status code is the signal: 201 means you created something new, 200 means you hit a replay. The response body is identical in both cases, so your code can treat them uniformly and use the status only for observability.
Replay returns the original stored response — it does not re-run routing or re-contact the provider. A replayed 200 is a guarantee that no second charge occurred.
Which endpoints accept idempotency keys
Use idempotency keys on create and charge mutations — any POST that has a side effect you would not want duplicated. The most important one is creating a transaction:
curl -X POST https://api.tokeflow.com/api/v1/transactions \
-H "Authorization: Bearer sk_live_mer_xxx" \
-H "Idempotency-Key: order_12345_attempt_1" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"currency": "BRL",
"payment_method": "credit_card",
"customer_id": "cust_7h2k9",
"payment_instrument_id": "pi_4a1c8",
"capture": true,
"metadata": { "campaign": "black_friday" }
}'First response (201 Created):
{
"success": true,
"data": {
"id": "tx_9a8b7c6d",
"status": "authorized",
"amount": 15000,
"currency": "BRL",
"payment_method": "credit_card",
"charge_type": "payment",
"customer_id": "cust_7h2k9",
"metadata": { "campaign": "black_friday" },
"created_at": "2026-01-15T12:30:00.000Z"
},
"request_id": "req_8f3c2a91",
"timestamp": "2026-01-15T12:30:00.000Z"
}Replaying the same Idempotency-Key returns the identical data with HTTP 200 OK.
Idempotency-Key is required on POST /api/v1/transactions. Omitting it returns 400 validation_error. This is intentional — payment creation must always be replay-safe.
{
"error": {
"type": "validation_error",
"code": "IDEMPOTENCY_KEY_REQUIRED",
"message": "Idempotency key is required. Provide it via Idempotency-Key header or idempotency_key in request body.",
"details": {},
"request_id": "req_1b2c3d4e",
"timestamp": "2026-01-15T12:30:00.000Z"
}
}TTL guidance
Idempotency keys are retained for a limited window, not forever. Treat the guarantee as covering the active lifetime of an operation and its retries — typically minutes to hours, well within any reasonable retry budget.
Practical implications:
- Retry an operation promptly. Don't reuse a key days later and expect a replay.
- For a genuinely new attempt after a long gap, mint a new key.
- Never depend on idempotency keys as a long-term deduplication store. Use your own order/transaction records for that.
See Errors for the full error-type reference and Transactions for the create-transaction contract.
Rate limits
The default limit
Each API key may make 100 requests per 60-second window. Limits are tracked per API key, so an Organization key and a Merchant key — or two different Merchant keys — each get their own budget.
| Property | Value |
|---|---|
| Limit | 100 requests |
| Window | 60 seconds (rolling) |
| Scope | Per API key |
| Exceeded status | 429 Too Many Requests |
| Error type | rate_limit_error |
When you exceed the limit, the request is rejected before it reaches the resource:
{
"error": {
"type": "rate_limit_error",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after a short delay.",
"details": {},
"request_id": "req_9z8y7x6w",
"timestamp": "2026-01-15T12:30:00.000Z"
}
}Need higher throughput for a specific workload (bulk imports, reconciliation jobs)? Discuss it during onboarding in the Tokeflow Dashboard — limits are configured per environment.
Designing for rate limits
A robust client treats 429 as an expected, recoverable signal rather than a failure.
- Back off exponentially. Wait progressively longer between retries (e.g. 0.5s, 1s, 2s, 4s) instead of hammering immediately.
- Add jitter. Randomize each delay so concurrent clients don't retry in lockstep and re-collide ("thundering herd").
- Cap attempts. Give up after a bounded number of retries and surface the error.
- Combine with idempotency. When you retry a
POSTafter a429, reuse the sameIdempotency-Keyso the eventual success can't double-charge. - Spread load. Batch where the API supports it, and avoid tight polling loops — prefer webhooks for state changes.
Retry with exponential backoff (Node)
This helper retries on 429 (and transient 5xx) using exponential backoff with full jitter. Because it forwards the same headers — including Idempotency-Key — on each attempt, retried mutations stay safe.
/**
* fetch wrapper with exponential backoff + jitter for 429 / 5xx.
* Pass a stable Idempotency-Key in `init.headers` so retried POSTs are replay-safe.
*/
async function tokeflowFetch(url, init = {}, { maxRetries = 5, baseMs = 500 } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, init);
const retryable = res.status === 429 || (res.status >= 500 && res.status <= 599);
if (!retryable || attempt >= maxRetries) {
return res; // success, non-retryable error, or out of attempts
}
// Exponential backoff with full jitter: random delay in [0, base * 2^attempt].
const delay = Math.floor(Math.random() * baseMs * 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Usage — idempotency key generated once and reused across all retries.
const res = await tokeflowFetch("https://api.tokeflow.com/api/v1/transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEFLOW_SECRET_KEY}`,
"Idempotency-Key": "order_12345_attempt_1",
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 15000,
currency: "BRL",
payment_method: "credit_card",
customer_id: "cust_7h2k9",
payment_instrument_id: "pi_4a1c8",
capture: true,
}),
});
const { data } = await res.json();Keep the secret key (sk_*) server-side only. The retry logic above belongs in your backend. Never embed a secret key in browser or mobile client code.
Putting it together
| Goal | Use |
|---|---|
| Prevent duplicate charges on retry | Idempotency-Key on POST /api/v1/transactions |
| Distinguish new vs replayed result | 201 (new) vs 200 (replay) |
Survive bursts and 429s | Exponential backoff + jitter |
| Avoid double-charging during backoff | Same idempotency key across all retries |
Related reading:
- Authentication — key types, scopes, and the
Authorizationheader. - Errors — every error type and HTTP-status mapping.
- Transactions — the full create-transaction contract.
- Webhooks — react to state changes instead of polling.