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:

  1. Return the same outcome for repeats. A request replayed with the same key must produce the same result: the same outcome and, when approved, the same holdReference, captureReference, releaseReference, or creditReference.
  2. Echo the key back. The response JWE must carry the request’s idempotencyKey under moov, 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 holdReference must return approved, even under a different idempotency key. Reuse the original captureReference if you can look it up.
  • Releasing an already-released hold must likewise return approved with the original releaseReference when available.
  • A holdReference you cannot resolve at all (aged out of your records) must return approved on 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:

SituationReturn
Ledger down, transient processing failure408, 429, or 5xx; Moov retries
Bad JWE, unknown provider, malformed claimsAnother 4xx; terminal, Moov stops
Business decision (declined payment, can’t capture)200 with denied / failed and a reason
Credit destination closed or invalid200 with destination_unavailable; Moov retries
Credit refused (you will not post this deposit)200 with refused; terminal, Moov begins reversal
Ledger down during credit408, 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.