← Back to home
← Back to the blog
5 min read· DirtFleet team

Idempotency keys in practice: the four-line integrator pattern

Every write endpoint in DirtFleet's public API accepts an Idempotency-Key. Here's how we store the binding, why we don't expire it, what we deliberately skipped, and the four lines of integrator code that make distributed systems survivable.

Idempotency is the single feature most integrators don't ask about until it bites them. A retry storm during a brief upstream blip duplicates 600 hours-logs across the fleet, the nightly accounting reconciliation breaks, and you spend a Saturday writing dedupe SQL. Designing for retries from day one costs ten lines of code per endpoint and saves the customer the Saturday. Here's how DirtFleet does it.

The contract

Every write endpoint in /api/v1/* accepts an Idempotency-Key request header. The value is any opaque string up to 128 characters — a UUID is fine; integrators sometimes use ${assetId}-${roundedTimestamp}. Two requests with the same key on the same endpoint return the same response, with no second insert.

Specifically: the first request creates the row + the key binding. Any subsequent request with the same key — even minutes later, even from a different client — looks up the binding, finds the original row, and returns the same { ok: true, logId: "...", ... } envelope. Status code is the same. Receiver can't tell whether the write was first-time or a replay; correctness doesn't depend on which.

Where the binding lives

The HoursLog row itself carries the key (column clientMutationId, unique per org). No separate dedupe table. This matters more than it sounds:

  • Race-free. A second concurrent request hits the same unique constraint Postgres uses for the first; one insert wins, the loser's findFirst finds the winner and returns it. No window where a race produces two rows.
  • No GC. Idempotency keys live as long as the row they bind to. If the row is deleted, the key disappears with it — there's no orphan-key cleanup cron to forget to write.
  • Tenant-safe by default. The unique index is on (organizationId, clientMutationId), so two orgs can pick the same key without colliding.

What gets retried

Three failure modes drive retries in fleet integrations:

  1. Network timeout. Receiver took the write but the response never came back. Without idempotency, the client retries and creates a duplicate. With idempotency, the retry gets the same response and no second row appears.
  2. 5xx from upstream. A Postgres replica timed out, the read after the write returned a slightly-stale response, our load balancer reset the connection. Same story — retry safely.
  3. Receiver crash. Your integration died between processing the response and updating its local state. On restart, it retries every pending write. With idempotency, this is fine; without it, the client has to track every pending write somewhere durable, or accept duplicates.

What we deliberately don't do

  • Per-endpoint key uniqueness. The key is unique per org, not per endpoint. A receiver generating one UUID per logical action can use the same key for the POST /hours and the follow-up POST /flags if both are part of the same logical operation. We'd rather an integrator over-share keys than have to manage a namespace.
  • Short TTLs. Some APIs expire idempotency keys after 24h. We don't — keys live as long as the underlying row. A receiver retrying a week later still gets the same response, which means a stuck cron eventually recovering cleanly is a non-event.
  • Body-hash binding. Stripe famously hashes the request body and rejects replays with the same key but different body. We considered it. It buys debuggability ("your retry changed the body, you must have a bug") but costs receiver code (you have to send the byte-identical body, which JSON serializers don't always guarantee across language pairs). For now we prefer the simpler contract: same key = same response, body-agnostic.

The four-line integrator pattern

// Generate one key per logical action. Persist it before you POST.
const key = crypto.randomUUID();
await db.pendingPosts.insert({ key, payload, status: "pending" });

// Now POST — retry-safe.
const res = await fetch("https://dirtfleet.app/api/v1/hours", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${DIRTFLEET_KEY}`,
    "Idempotency-Key": key,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(payload),
});

// On success or failure, mark the local row. A crash here means the
// next restart retries; DirtFleet returns the original logId.
await db.pendingPosts.markDone(key, res.status, await res.json());

The shape is the same in every language. Generate a key, persist it before the network call, retry until the local row is marked done. Idempotency doesn't make distributed systems easy — it makes them survivable.

→ Full Node.js examples · → API reference · → Why integration-first design