Accounting software must never submit the same salary data twice. Network
failures, timeouts, and process restarts make it impossible to know whether a
request reached the server unless you track the answer explicitly. The
Idempotency-Key header lets you retry safely: if the server already processed
your request, it replays the original response without re-running the operation.
Idempotency-Key is required on POST /v1/salary-rows and
POST /v1/salary-rows/replace. Supply a new UUID for each distinct submission.
POST /v1/salary-rows HTTP/1.1
Host: api.fontes.ee
Authorization: Bearer <token>
Idempotency-Key: 7f3c9e1a-d8b2-4f6e-a0c5-1234567890ab
Content-Type: application/json
Generate the key before you send the request, and persist it (in your database or state store) alongside the submission record. If the request fails with a network error or server error, use the same key to retry.
If the server has already processed a request with this key and the request
body is identical, the original response is replayed immediately without
re-executing the import pipeline. The replayed response includes an
Idempotency-Replayed: true header so you can distinguish it from a fresh
processing result.
This is the primary retry case. Use it freely.
If you send the same Idempotency-Key with a different request body, the
server returns 422 idempotency_key_reused. This protects you from accidents:
if you accidentally reuse a key from a different submission, the server refuses
to process it rather than silently overwriting data.
Generate a new key whenever you change the body, including after fixing validation errors in a previous request.
A key the server has never seen is processed from scratch. The row-level upsert logic in merge mode provides an additional layer of protection: even if you accidentally skip the idempotency key and submit twice, the second call upserts the same rows and produces the same result (no duplicates).
The server retains idempotency keys for 24 hours from the time of the original request. After 24 hours, a retry with the same key is treated as a fresh request and processed normally.
Design your client to retry within the 24-hour window. If an outage lasts longer than 24 hours, record the gap in your own state and start a new submission with a new key after recovery.
Use exponential backoff with jitter when retrying failed requests.
| Attempt | Delay before retry | Condition to retry |
|---|---|---|
| 1st retry | 250 ms | Network error, 5xx |
| 2nd retry | 1 000 ms | Network error, 5xx |
| 3rd retry | 4 000 ms | Network error, 5xx |
| Give up | — | After 3 retries |
Retry on: network errors (connection reset, timeout), 500, 502, 503,
504.
Do not retry on: 400, 401, 403, 422, 429. These are definitive
answers from the server. For 401, re-issue a JWT first and then retry the
original request with the same idempotency key.
Jitter: add ±20 % random noise to each delay to avoid thundering-herd problems when many integrations hit the same transient failure.
// Minimal retry helper (illustrative — use Polly in production)
var delay = TimeSpan.FromMilliseconds(250);
for (var attempt = 0; attempt < 3; attempt++)
{
var resp = await http.PostAsJsonAsync("/v1/salary-rows", body);
if (resp.IsSuccessStatusCode || (int)resp.StatusCode < 500) return resp;
await Task.Delay(delay + TimeSpan.FromMilliseconds(Random.Shared.Next(-50, 50)));
delay *= 4;
}
The API allows 10 requests per minute per IP on anonymous endpoints and a
higher limit on authenticated endpoints. If you receive 429 rate_limit_exceeded,
back off for at least 60 seconds before retrying. The Retry-After header
(when present) tells you the exact wait time in seconds.
4xx responses (except 401 with a fresh JWT).