Idempotency
Networks fail. Clients time out. Retrying a failed request is the right thing to do — but retrying a request that already succeeded can create duplicate data. Idempotency solves this.What idempotency means
An idempotent request is one you can send multiple times and get the same outcome as if you sent it once. The second send does not create a second record — it returns the result of the first. This matters most for write operations (creating or modifying data). A journal created twice is an accounting error. Idempotency lets your retry logic be simple and aggressive without risk.The Idempotency-Key header
To make a request idempotent, include an Idempotency-Key header:
What happens on retry
| Scenario | What the API does |
|---|---|
| First request succeeds, you retry with the same key | Returns the original 200 response, operation not repeated |
| First request is still in progress, you retry | Returns 409 Conflict — wait and retry again |
First request failed with a 4xx error | Not cached — you may retry with the same key |
| No key provided | Request is not idempotent, normal behavior |
200 response immediately without touching the database again. You will see the same id, serialNumber, and any other fields from the original response.
Which endpoints support idempotency
| Endpoint | Supported |
|---|---|
POST /Companies — create a company | Yes |
POST /Companies/{companyId}/Accounts — create an account | Yes |
POST /Companies/{companyId}/CostCenters — create a cost center | Yes |
POST /Companies/{companyId}/FinancialYears — create a financial year | Yes |
POST /Companies/{companyId}/Journals — create a journal | Yes |
PUT /Companies/{companyId}/Journals/{id} — update a journal | Yes |
POST /Companies/{companyId}/Journals/{id}/Reverse — reverse a journal | Yes |
POST /Companies/{companyId}/FinancialYears/{id}/GenerateClosingJournal — generate closing journal | Yes |
PUT /Companies/{companyId}/FinancialYears/{id}/Close — close a financial year | Yes |
PUT /Companies/{companyId}/FinancialYears/.../Periods/{id}/Close — close a period | Yes |
Key lifetime
Idempotency keys expire 24 hours after the first request is received. The clock starts when the server first sees the key, regardless of whether that request succeeded or failed. After expiry, a request with the same key is treated as a brand new request — the server has no memory of it. Generate a fresh key for each new logical operation. Do not reuse a key across different operations even after its window expires.Technical reference
Key format and constraints
| Property | Value |
|---|---|
| Maximum length | 255 characters |
| Allowed characters | Any UTF-8 string |
| Case sensitivity | Case-sensitive — ABC and abc are different keys |
| TTL | 24 hours from first receipt |
| Scope | Per company + per operation type |
Scope
The key is scoped to a company and operation type. The triple(companyId, operationType, key) must be unique. This means:
- The same key string used for
POST /JournalsandPOST /FinancialYearsdoes not conflict — they are different operation types. - The same key string used under company A and company B does not conflict — they are different companies.
- For
POST /Companies(company creation), the key is scoped to the authenticated user rather than a company, since no company exists at that point.
How the server enforces uniqueness
Token creation uses a database-level unique constraint. If two requests with the same key arrive simultaneously, only one can insert the token — the other receives a409 Conflict immediately. This guarantee holds even under high concurrency without any client-side coordination.
What gets cached
Only successful200 responses are stored. The full response body is serialized and held for the TTL window.
For endpoints that return no body (closing a financial year or a period), the server stores an internal sentinel on success. Retries for these endpoints return 200 with an empty body — the same as the original response.
Error responses (4xx, 5xx) are never cached. The token is discarded on failure, so the same key can be retried with a corrected request.
Choosing a key
The key must uniquely identify one logical operation. A UUID v4 generated at the point of user intent is the simplest and safest choice:Handling a 409 response
A 409 means a request with your key is currently in flight on the server — the first attempt has not completed yet. This is transient. Wait briefly and retry:
- Receive
409 - Wait 1–2 seconds
- Resend with the same key and body
- Repeat until you get a non-
409response
Retry strategy
A safe pattern for any write request:- Generate a key before the first attempt.
- Store the key locally (in memory, in a job queue, etc.) so you can reuse it.
- Send the request.
- On network error or
5xx: wait with exponential backoff and retry with the same key. - On
409: wait 1–2 seconds and retry with the same key. - On
4xx(except409): do not retry automatically — the request was rejected, fix it first. - On
200: the operation succeeded. Discard the key.
Important rules
- Never change the request body between retries. If the body changes, the key is meaningless — the server will return the response from the original body. Use a new key for a new request.
- Keys are per operation, not per session. Do not reuse a key for two different journals or two different operations.
- Keys are per company. A key used under company A and company B are independent — they do not conflict.
