Idempotency
Retry mutations safely with permanent keys and the same canonical request.
Every merchant POST requires an Idempotency-Key header. Create and persist a key before sending the request; reuse it when the outcome is unknown.
Usage#
curl --request POST 'https://api.thehalfin.com/v1/invoices' \
--header "X-API-Key: $HALFIN_API_KEY" \
--header 'Idempotency-Key: order-1001-invoice' \
--header 'Content-Type: application/json' \
--data '{
"amount": "25.00",
"currency": "USD",
"external_id": "order-1001"
}'The header contains 1–255 printable ASCII characters without spaces. UUIDs are a convenient choice, but UUID format is not required. Supply one unquoted key value; do not put it in the JSON body.
Scope and lifetime#
Keys are scoped to the merchant and environment, shared by all API keys in that scope. Use a distinct key for each logical mutation, including create, confirm, and cancel. Reusing a key on another operation or resource conflicts.
Once an outcome is stored, the key does not expire and cannot be recycled. An exact retry returns the original business outcome and Idempotent-Replayed: true. It does not create another resource or move funds again. A replayed creation response may show an earlier status; retrieve the resource to read its current state.
What counts as the same request#
The fingerprint covers the operation, path parameters, query, and entire canonical JSON body. JSON object-key ordering and insignificant whitespace do not matter. Array order and field values do matter, including annotations such as description, metadata, and auto_confirm.
| Retry change | Result under the same key |
|---|---|
| JSON formatting or object-key order | Same request |
"10" changed to "10.00" | 409 conflict |
| A currency's letter case changed | 409 conflict, even when the value normalizes to the same currency |
| Metadata or description changed | 409 conflict |
An identical request still executing returns 409 in_flight. Wait and retry the original request with the same key.
Released keys#
Idempotency-Key-Released: true means the server confirmed that this attempt left no accepted outcome bound to the key. After correcting the refused request, you may reuse that key. A concurrent request can still claim it.
Do not infer release from a 400, 404, 409, or 422 status alone. Not every refusal releases a reservation. If the header is absent and the outcome is uncertain, preserve the original request and key; do not try a new key to bypass the uncertainty.
Retry strategy#
| Outcome | Action |
|---|---|
| Success or replay | Save the returned resource ID; use GET for its current state |
Network timeout, connection loss, 500, or 503 | Back off with jitter and retry the same key and body |
409 in_flight | Retry the same request after a delay |
409 conflict | Stop; compare the persisted original request with the new request |
| Confirmed release | Correct the refusal and reuse the key if appropriate |
401 / 403 | Fix authentication or permissions; do not spin retries |
Respect Retry-After when supplied. Keep retry attempts bounded and escalate an unresolved result for reconciliation. Webhook deduplication uses the event id; it is independent of request idempotency keys.