Idempotency

Network calls fail halfway. Idempotency keys let you retry a create safely without risking a duplicate invoice, client, or expense.

Every create takes an Idempotency-Key

Each POST that creates a resource requires an Idempotency-Key request header. Generate a unique value per logical create — a UUID is ideal:

curl -X POST https://billey.nl/api/v1/clients \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff" \
  -H "Content-Type: application/json" \
  -d '{ "client_type": "client", "name": "Example Client B.V.", "country": "NL", "currency": "EUR", "locale": "nl", "collection_method": "invoice" }'

How replays behave

  • The first request with a given key creates the resource and returns 201 Created.
  • Any replay with the same key creates nothing and returns the original resource with 200 OK.

So if a create times out, just send it again with the same key: you either created it the first time (and now get the original back) or you create it now. Either way you end with exactly one record.

Keys are scoped per book, so two different books can independently use the same key value without colliding.

Key requirements

The header must be non-empty, valid UTF-8, free of control characters, and at most 255 bytes. Anything else is rejected with 400 Bad Request before the create is attempted.

What does not take a key

Idempotency keys apply to creates only. These do not accept an Idempotency-Key header:

  • PATCH (updating a resource) and DELETE.
  • The invoice lifecycle transition endpoints — send, mark-sent, mark-paid, and revert-to-draft.

PATCH and DELETE are naturally idempotent (applying the same update twice lands the same state), so they need no key. The invoice transitions are guarded by state instead: a transition that no longer makes sense returns 409 Conflict. Note that POST /api/v1/invoices/{invoice}/credit-note is a create and does take a key.

Something inaccurate or missing? support@billey.nl