Idempotency
Networks drop, jobs retry, admins double-click. An idempotency key turns any of that into exactly one payment.
How it works
Send an Idempotency-Key with a create request. FomoPay stores the key with the payment, unique per project, forever. When the same key comes back:
| Request | Result |
|---|---|
| Same key, same request | The original payment, HTTP 200, idempotent_replayed: true and the header Idempotent-Replayed: true. Nothing new is created or paid — whatever status the original has now is what you see. |
| Same key, different request | 409 DUPLICATE_PAYMENT with details.payment_id of the original. Nothing is created. |
| New key | A new payment. |
What counts as “the same request”
FomoPay hashes a normalized form of the request: the recipient handle (case-insensitive, with or without @), the amount as a number ("100" equals "100.0"), the asset, memo, reason, metadata, vault, visibility and source. Header order, JSON key order and whitespace don't matter. Risk acknowledgements (confirmations) are not part of it, so you can retry a 428 CONFIRMATION_REQUIRED with the same key.
Where keys apply
| Endpoint | Key | Notes |
|---|---|---|
POST /v1/rewards | Required | Header or idempotency_key in the body (if both, they must match). 1–255 printable characters, no spaces. |
POST /v1/payments | Recommended | Header or body field. The SDK always sends one. |
POST /v1/payments/bulk | Recommended | Same key + same rows → the same batch. |
A request that fails validation or is refused (unknown recipient, vault short of funds, over a limit) creates nothing, so the key stays unused — fix the cause and retry with the same key.
Choosing keys
- Derive the key from the thing you're paying for, not from the attempt:
FOMORUN_42_WINNER,BOUNTY_8812,REFERRALS_2026_W41. Then every code path that tries to pay round 42's winner — retries, a second worker, a redeploy mid-payout — converges on one payment. - Never use a random key per attempt for business events. It protects one HTTP call, not your process.
- Keys are permanent per project. A key used once is bound to that payment for good — it can't be reused for a different payout later.
The SDK and retries
The SDK retries 429, 5xx, timeouts and network errors with exponential backoff (500 ms, 1 s, 2 s … capped at 8 s, ±25 % jitter, honouring Retry-After) and always reuses the same key. send, payments.create and bulk.* generate a key if you don't pass one, so the SDK's own retries are always safe; requests without a key (like creating a webhook) are only retried on 429, which the API rejects before doing any work.
If your process crashes between “call FomoPay” and “record that I called FomoPay”, a fresh process will generate a fresh key. For anything that matters, pass your own deterministic idempotencyKey.