Idempotency and retries
Networks fail and timeouts happen, so Moov will call your endpoints more than once. Your implementation must make repeated delivery safe.
The idempotency key #
Every request carries an idempotencyKey under moov, chosen by Moov
Money. Your obligations:
- Return the same outcome for repeats. A request replayed with the same
key must produce the same result: the same
outcomeand, when approved, the sameholdReference,captureReference,releaseReference, orcreditReference. - Echo the key back. The response JWE must carry the request’s
idempotencyKeyundermoov, unchanged. Moov rejects the response on a mismatch.
Keys are scoped per endpoint #
The dedupe scope of an idempotency key is per endpoint, not global. Moov Money derives per-payout keys that may collide across authorize, capture, and release, so a key you saw on authorize arriving later on capture is a new operation, not a replay. Treating keys as endpoint-scoped is required for correctness. Credit keys are scoped to one disbursement attempt; a redelivered credit with the same key is the same operation, not a second deposit. See credit account.
For authorize specifically, the key’s scope is Moov Money’s payout: the same key will never be reused for a different payout.
Reference-based idempotency #
Capture and release add a second layer keyed on the hold itself:
- Capturing an already-captured
holdReferencemust returnapproved, even under a different idempotency key. Reuse the originalcaptureReferenceif you can look it up. - Releasing an already-released hold must likewise return
approvedwith the originalreleaseReferencewhen available. - A
holdReferenceyou cannot resolve at all (aged out of your records) must returnapprovedon capture. Moov treats unknown holds as already captured rather than stranding funds. Escalate to Moov before implementing anything else here.
Retry policy #
Moov Money retries on transport errors and on 408, 429, and 5xx
responses. All other 4xx responses are terminal.
Use that split deliberately:
| Situation | Return |
|---|---|
| Ledger down, transient processing failure | 408, 429, or 5xx; Moov retries |
| Bad JWE, unknown provider, malformed claims | Another 4xx; terminal, Moov stops |
| Business decision (declined payment, can’t capture) | 200 with denied / failed and a reason |
| Credit destination closed or invalid | 200 with destination_unavailable; Moov retries |
| Credit refused (you will not post this deposit) | 200 with refused; terminal, Moov begins reversal |
| Ledger down during credit | 408, 429, or 5xx; Moov retries the same key |
Never signal a processing failure as a 200 with an unmodeled outcome.
Moov Money records it as unknown and discards your reason. Non-200
response bodies are not parsed today, so a status code is all Moov sees on
failure.
Timestamps #
Requests are issued with exp = iat + 5 minutes at the top of the
inner JWT. Reject requests where exp has passed or iat is
implausibly far in the future, allowing a small clock-skew leeway (30
seconds is typical). Your responses must carry their own iat and
exp at that same level, not inside moov.