The two rules that make retries safe
Send a partner-generated UUID in theIdempotency-Key HTTP header on every mutating call, and branch your error handling on the typed string in error.code (never on a bare HTTP number). Those two habits turn a flaky network into a non-event: you can retry a payment or a top-up without ever double-acting.
This page covers the model and the codes you will hit most. The complete code-by-code table lives at Error Reference.
Idempotency: one key, one meaning
TheIdempotency-Key HTTP header is the single deduplication key for the whole API. Nothing else dedupes a request.
required
A partner-generated UUID. Required on every mutating call. Optional on the side-effect-free read-shaped
POSTs (identify, redeem balance check, evaluate incentives): sending it there is accepted, just not required. 24h TTL. One key represents one logical operation, stable across your retries and unique across different operations.meta.idempotency_key field. Generate the UUID once when you first attempt the operation, store it next to your local order record, and reuse that same value on every retry of that one operation.
Same key, same payload: byte-identical replay
Send the same key with the same request payload and Feddi replays the original outcome. You get the original HTTP status and the byte-identical response body, plus a flag on the envelope:true on a replay. The work already ran once; this response is a copy of that first result, not a fresh execution.Same key, different payload: a typed conflict
Reuse a key with a different payload and Feddi rejects it withIDEMPOTENCY_KEY_REUSED (HTTP 422). This signals that your key generation is broken: a key must be stable for one logical operation and unique across different ones. Two genuinely different operations sharing a key would otherwise let the second one silently inherit the first one’s result, so the API refuses.
partner_request_id is correlation, not dedup
meta.partner_request_id in the request envelope is your correlation id. It is for tracing and reconciliation, never for deduplication. The distinction is sharp:
Fresh request_id, reused key
A REPLAY. No new debit. You get the original result back with
idempotency_replayed: true.Reused request_id, fresh key
A NEW operation. A new debit runs. The repeated
partner_request_id is just a correlation collision, not a guard.Concurrent sends of the same key
Fire the same key twice at once (a double-tap, a retry that races the original) and exactly one request executes. The rest wait for the winner and then replay its result. You never get two debits from one key, even under a race.Defense in depth on payments
Idempotency is the outer guard.POST /payments also protects the inner steps: the OTP minted by POST /payments/otp/send (the trigger leg) is consumed atomically with the debit, so a failed debit (for example INSUFFICIENT_FUNDS) does not consume the OTP and the credential stays retryable. QR nonces are single-use; a replayed nonce returns CREDENTIAL_EXPIRED_OR_REPLAYED. Read Identity & Credentials for the credential lifecycle.
Inbound provider webhooks are a separate contract
The header rule above is the outbound contract: you calling Feddi. Inbound provider webhooks (a settlement notification arriving at Feddi) follow a separate contract. They carry noIdempotency-Key header and deduplicate on the provider’s own settlement reference. Do not assume the outbound idempotency model applies to inbound delivery. See Webhooks for the verification contract.
Errors: typed string codes you branch on
Every error is a typed object in the response envelope, never a bare HTTP number:error.code, the string enum. Read error.message for a human-readable summary, and read error.details for the machine-actionable specifics: the shortfall on a declined payment, the valid set on an unsupported enum, the candidate programs on an ambiguous customer. Typed errors that reference a valid set always echo it in details so you can recover without a second round trip.
The codes you will hit most
HTTP 401. Missing or invalid
x-api-key, or an expired or malformed terminal JWT. Re-authenticate and refresh the terminal JWT.HTTP 403. Valid key, but not authorized for this merchant or branch scope (or a
PENDING_PROOF wallet on a payment). On a transaction read, 403 means the transaction exists but belongs to another integration. Check your tenant scope, or route the customer to enrollment.HTTP 404. Resource genuinely absent. Some lookups also collapse cross-tenant ids to 404 so existence and PII never leak, but transaction reads do not: there a cross-tenant id is a 403
FORBIDDEN, and 404 means the id is genuinely unknown. Confirm the ids belong to your tenant.HTTP 400. Malformed body, missing field, or a domain rule was violated. Read
details for field errors, fix, then retry with a NEW key.HTTP 402. Actual plus promotional balance cannot cover the debit. Route to top-up or recovery. The credential is NOT consumed, so the retry is valid after a reload.
HTTP 422. Same key, different payload. Fix key generation. Do not retry with a fresh key as a workaround.
HTTP 422. The customer resolves to more than one program. Pass
wallet_program_id from details.candidates.HTTP 422. No active provider for the requested currency. Use a currency from
details.supported.HTTP 429. Too many requests. Back off per the
Retry-After header and details.HTTP 500. A Feddi-side error. Retry with the SAME key (safe replay). Escalate if it persists.
INTERNAL_SERVER_ERROR is replay-safe
A 500 does not mean your operation failed and must be re-issued fresh. It means Feddi could not confirm the outcome to you. Retry the exact same request with the sameIdempotency-Key. If the work already committed, you get the real result replayed; if it did not, the retry runs it once. Either way you end with exactly one effect.
Never retry a 500 with a fresh
Idempotency-Key. A fresh key turns a safe replay into a possible double-debit, because the API has no way to recognize the retry as the same operation.Where to go next
API Conventions
The envelope, headers, and versioning rules every call inherits.
Error Reference
The complete typed-error table, code by code.
Payments & Redemption
The atomic debit where the replay and credential guards matter most.
Webhooks
The event catalog, the event envelope, and the signature-verification contract.