Idempotency & Retries

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.


The Idempotency-Key header

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.


The idempotency contract

Same key + same body → cached response

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.

Same key + different body → 422

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.

New key → processed normally

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).


Key lifetime

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.


Retry policy guidance

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;
}

Rate limiting

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.


Practical checklist