Errors and idempotency
RFC 7807 problem details, Idempotency-Key, ETag optimistic concurrency, and rate limits for the Fuxam API v4.
When something goes wrong — or when you intentionally retry — the Fuxam API v4 follows a small set of strict rules: RFC 7807 problem documents for errors, Idempotency-Key for safe creates, ETag / If-Match for safe updates and deletes, and RateLimit-* / Retry-After when you hit the shared budget. Learning these once makes every endpoint safer to automate.
Why it matters
Integrations fail in the real world: networks drop mid-POST, two jobs update the same user, a sync loop exceeds 1000 requests per minute. Without idempotency and concurrency controls you risk duplicate records or silent overwrites. Without a stable error code you cannot branch reliably in client code. This chapter is the contract for those failure modes.
How it fits
Read this alongside the Overview summary, Authentication for 401 cases, and Recipes for copy-ready patterns.
Key concepts
| Concept | Meaning |
|---|---|
| Problem details | RFC 7807 JSON body with Content-Type: application/problem+json. |
| ApiErrorCode | Closed set of stable top-level code values (for example resource_not_found). |
| Zod issue code | Per-field validation codes inside errors[] (for example invalid_string) with RFC-6901 pointers. |
| Idempotency-Key | Client-generated key that makes a create POST replay-safe within the idempotency window. |
| ETag / If-Match | Optimistic concurrency: write only if the resource version still matches. |
| Rate limit | 1000 requests/min per integration + institution (sliding window). |
Error model (RFC 7807)
All errors use application/problem+json. The type field is the URI https://fuxam.app/errors/{code}.
There are two distinct code namespaces:
| Namespace | Where | Purpose |
|---|---|---|
Top-level code |
Root of the problem document | Closed ApiErrorCode union — machine-readable and stable across versions of your client |
errors[].code |
Optional array entries | Zod issue codes (for example invalid_string) with an RFC-6901 pointer (for example /email) |
Example:
{
"type": "https://fuxam.app/errors/resource_not_found",
"title": "Not Found",
"status": 404,
"code": "resource_not_found"
}
No 403 — privacy by uniformity
v4 emits no 403. Cross-tenant access returns the same 404 resource_not_found as a truly missing resource. Existence of another institution’s ids is never leaked. Client code should treat 404 as “not available to you,” not only “never existed.”
Validation failures return HTTP 422.
Common status codes
| Status | Typical meaning |
|---|---|
200 |
Successful read or update |
201 |
Resource created |
204 |
Resource deleted (no body) |
400 |
Bad request (for example invalid_cursor) |
401 |
Authentication failed |
404 |
Missing resource or cross-tenant denial (resource_not_found) |
409 |
Conflict, or in-flight idempotency lock |
412 |
ETag mismatch (precondition_failed) |
422 |
Validation failure |
428 |
Required If-Match missing (precondition_required) |
429 |
Rate limit exceeded — honor Retry-After |
Idempotency for creates
POST requests that create resources accept an Idempotency-Key header. Submitting the same key twice within the idempotency window returns the cached response (including the original 201) without creating a duplicate. Conflicting in-flight keys may return 409.
POST /api/v4/users HTTP/1.1
Host: fuxam.app
Authorization: Basic <base64(clientId:clientSecret)>
Content-Type: application/json
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Accept: application/json
IDEMPOTENCY_KEY="$(uuidgen)"
curl -sS \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d @create-body.json \
"https://fuxam.app/api/v4/users"
Use the JSON body documented on the create operation’s endpoint reference — the important part of this pattern is reusing the same Idempotency-Key on retries.
Optimistic concurrency (ETag)
Single-resource GETs emit a strong ETag. Send it back as If-Match on PATCH, PUT, or DELETE:
| Outcome | Status / code | Meaning |
|---|---|---|
| Version still current | Success (200 / 204) |
Write applied |
| Resource changed since read | 412 precondition_failed |
Re-GET, merge, retry with new ETag |
If-Match required but missing |
428 precondition_required |
Include If-Match |
GET /api/v4/users/usr_123 HTTP/1.1
Host: fuxam.app
Authorization: Basic <base64(clientId:clientSecret)>
Accept: application/json
Response includes:
ETag: "abc123version"
Then:
PATCH /api/v4/users/usr_123 HTTP/1.1
Host: fuxam.app
Authorization: Basic <base64(clientId:clientSecret)>
Content-Type: application/json
If-Match: "abc123version"
Accept: application/json
ETAG='"abc123version"'
curl -sS \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-X PATCH \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "If-Match: ${ETAG}" \
-d @patch-body.json \
"https://fuxam.app/api/v4/users/usr_123"
Put the fields you intend to change in patch-body.json per the endpoint reference; always pair the write with the ETag you just read.
Rate limits
Each integration shares a 1000 requests/min budget (sliding window, keyed by integration + institution). Responses include RateLimit-* headers. When you exceed the budget:
- Status:
429 - Header:
Retry-After(honor it before retrying)
Retry and backoff guidance
Treat retries as a layered policy:
429— sleep forRetry-Afterseconds (or the suggested delay your client parses), then retry the same request.- Transient network failures on safe reads — retry with exponential backoff and jitter.
- Transient failures on creates — retry with the same
Idempotency-Key. 412on writes — do not blind-retry the same body with the old ETag; re-read, reconcile, send a newIf-Match.422/401/ most400s — fix the request; retries without change will keep failing.
Best practices
- Branch on top-level
code, not only on human-readabletitletext. - Surface
meta.requestIdfrom successful calls and any identifiers returned in problem documents when logging failures. - Always send
Idempotency-Keyfrom automated create jobs. - Always send
If-Matchon single-resource writes that require it. - Map
404to a single “unavailable” path in UI or sync logic — do not special-case a fictional403.
Common pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| Retrying POST without Idempotency-Key | Duplicate resources | Generate and reuse one key per logical create |
| Ignoring 412 | Overwrite races or endless retries | Re-GET and merge |
| Treating 404 as “delete from local cache” always | May hide permission/tenant issues | Confirm with a known-good id first |
| Busy-looping on 429 | Longer lockout, wasted compute | Honor Retry-After |
FAQ
What is the difference between the two code fields?
Top-level code is the stable API error classification. Entries in errors[].code describe individual validation issues (Zod) and include a pointer to the failing field.
Will I ever see HTTP 403?
Not from v4’s cross-tenant denial path — that is intentionally 404. Design clients accordingly.
How long is the idempotency window?
The overview documents that replays within the idempotency window return the cached response. Treat the key as valid for reasonable retry periods; do not rely on keys forever — use a new key for a new logical create.