Skip to content
Ledgerline Help

Idempotency keys

Updated 5 September 20263 min read

On this page

A network call can fail after the server did the work, leaving you unsure whether your payout exists. Idempotency keys remove the doubt. Send the same key on a retry and Ledgerline performs the operation once, then hands back the original result every time.

Using the header

Add an Idempotency-Key header to every POST request. GET and DELETE requests are naturally idempotent and ignore the header.

curl https://api.ledgerline.dev/v1/payouts \
  -H "Authorization: Bearer $LEDGERLINE_API_KEY" \
  -H "Idempotency-Key: 7d2f8c1e-5b3a-4a96-8a1e-9c04d2b6f3e8" \
  -d source_account_id=acc_01J9QK5M2A \
  -d counterparty_bank_account_id=cba_01JA6C8W4N \
  -d amount=1250000 \
  -d currency=USD \
  -d rail=ach
Rule Value
Header name Idempotency-Key
Maximum length 255 characters
Recommended format A random UUID v4
Scope Per API key and endpoint
Retention 24 hours from the first use

Keys are scoped to the API key that sent them, so two services using different keys cannot collide.

What a replay looks like

When Ledgerline sees a key it has already completed, it does not run the operation again. It returns the stored response with the same status code and body, and adds a header:

HTTP/1.1 201 Created
Idempotent-Replayed: true
Ledgerline-Request-Id: req_01JA8F1ZPM

The Idempotent-Replayed: true header tells you the response came from the store. The Ledgerline-Request-Id is the id of the original request.

When the key is reused wrongly

Situation Status Code
Same key, same parameters, first request finished Original status none, replayed
Same key, different parameters 422 idempotency_key_mismatch
Same key while the first request is still running 409 idempotency_conflict

The mismatch error protects you from a bug where a key is reused for two logically different payments. Parameters are compared on the request body and path, not on headers.

For idempotency_conflict, wait a moment and retry with the same key. The first request will finish and the retry will return its result.

What is stored

Ledgerline stores the result of requests that reached a definite outcome:

  • Stored: 2xx successes and 4xx client errors. Retrying a request that failed validation returns the same validation error, which is what you want, because the input did not change.
  • Not stored: 5xx server errors and 429 rate limit responses. The operation may not have run, so a retry with the same key executes it afresh.

Warning

Because a 4xx is replayed for 24 hours, you cannot fix a bad request and resend it under the same key. Change the key whenever you change the request. See Returns and failed payouts for the same rule applied to retried payouts.

Choosing good keys

A key should identify the intent, not the attempt. Generate it once per logical operation, store it with your own record, and reuse it on every retry of that operation.

  • Good: a UUID saved on your invoice_payment row before the first call.
  • Good: a deterministic key such as payout:invoice-2041:v1, when you can guarantee uniqueness.
  • Bad: a new random key generated inside the retry loop. It defeats the purpose.
  • Bad: a timestamp. Two concurrent requests can share it.

Client libraries

The official SDKs create a key for you when you do not pass one, and reuse it across their own automatic retries. Pass your own when the operation spans your retries too. See Node SDK and Python SDK.

await ll.payouts.create(params, { idempotencyKey: invoice.paymentKey });
client.payouts.create(**params, idempotency_key=invoice.payment_key)

Idempotency and external ids

Some resources accept an external_id that is unique within a ledger, for example transactions. It is a second line of defense that survives the 24-hour retention of idempotency keys, because a duplicate external_id fails with conflict_error for as long as the transaction exists.