POST /v1/payouts is idempotent. Retry the same logical request as many
times as you need — the API will return the original payout instead of
creating a new one.
How keys are picked
Two sources, in order:
Idempotency-Key header, if you send one. Any opaque string is fine
— a ULID, a UUID, or a deterministic key derived from your payroll run
(e.g. payroll-co-2026-05-emp-001).
external_id from the body, if no header is provided. Every payout
already requires a unique external_id, so this gives you idempotency
“for free” without managing a separate key.
Either way, the key is scoped per API key. Two different tenants can use
the same external_id without colliding.
Replay semantics
| Scenario | Response |
|---|
| First call with key K, body B | 201 Created — payout created |
| Replay with key K, body B (identical) | 200 OK — same payout returned |
| Same key K, different body | 409 IDEMPOTENCY_KEY_REUSED |
Same external_id (no idempotency header match) | 409 EXTERNAL_ID_CONFLICT |
Note the status code: idempotent replays return 200, not 201. This
lets your client tell “I just created this” from “I called this same request
again”.
Recommended pattern
Derive an idempotency key from the payroll run that produced the
net_salary you’re paying out. A good key is deterministic (so retries
match) and unique per logical payout (so unrelated requests don’t
collide).
curl -X POST https://api.clevis.dev/v1/payouts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: payroll-co-2026-05-emp-001" \
-H "Content-Type: application/json" \
-d '{
"amount": "4600000.00",
"currency": "COP",
"country": "CO",
"external_id": "payroll-co-2026-05-emp-001",
"beneficiary": { ... }
}'
What counts as “identical body”?
The whole JSON request body. If you change any field — amount, beneficiary
details, metadata — and reuse the same Idempotency-Key, you get
409 IDEMPOTENCY_KEY_REUSED. That is intentional: it surfaces caller bugs
(e.g. you re-derived the amount but kept the key) instead of silently paying
out something different from the original intent.
Idempotency-Key vs. external_id
| Idempotency-Key | external_id |
|---|
| Where it lives | Request header | Request body |
| Required? | Optional | Required |
| Visible on the payout resource | No | Yes |
| Used to dedupe | Yes (preferred) | Yes (fallback) |
Searchable via GET /v1/payouts?external_id=... | No | Yes |
If you only ever set external_id (and skip the header), you still get full
idempotency. The header exists for cases where you want to retry the same
logical create with a fresh external_id — uncommon, but supported.
Idempotency window
Keys are remembered for the lifetime of the in-memory store. Since the v1
mock store is process-local and ephemeral, restarting the API drops all
idempotency keys. Don’t rely on the mock for cross-process deduplication
testing. The real dLocal-backed implementation will use a persistent
window (matching dLocal’s own retention).