Skip to main content

Idempotency

Moonlight's ingest API supports the Idempotency-Key header for exactly-once semantics over flaky networks. Repeating a request with the same key returns the cached response without re-running the worker.

Header Contract

POST /api/v1/ingest
X-API-Key: <key>
Idempotency-Key: neary:purchase:100:paid:v1
Content-Type: application/json

{ "entries": [...] }
  • Keys are arbitrary strings up to 255 characters. Use a stable, deterministic value per business operation (e.g. purchase:{id}:paid in Neary).
  • Records are stored in idempotency_record (V16 migration) for 24 hours. After that the key is reusable.
  • Replays are scoped per (integration_id, request_path, request_method), so the same key can be reused across distinct endpoints.

Conflict Detection

If the same key is sent with a different request body fingerprint, the server returns 409 Conflict:

{
"statusCode": 409,
"message": "Idempotency-Key was reused with a different payload",
"error": "Conflict"
}

The fingerprint is a SHA-256 of the canonical JSON representation of the request body (key order normalised). Adding optional whitespace or reordering keys does not trigger a conflict.

SDK Support

The MoonlightClient accepts the key as an option:

await client.ingest(
{ entries: [...] },
{ idempotencyKey: 'purchase:100:paid:v1' },
);

The client also retries 5xx/429/network errors with exponential backoff (configurable via maxRetries), so transient failures don't require operator intervention.

Storage

idempotency_record (
key text PRIMARY KEY,
integration_id UUID NOT NULL,
request_path text NOT NULL,
request_method text NOT NULL,
request_fingerprint text NOT NULL,
response_status integer NOT NULL,
response_body jsonb,
created_at timestamptz NOT NULL DEFAULT NOW(),
expires_at timestamptz NOT NULL
)

A scheduler tick (IDEMPOTENCY_RECORD_GC) garbage-collects expired rows hourly.

When NOT to Use

  • Retries already in-flight — the SDK's built-in retry already covers transient errors; Idempotency-Key is for application-level redoes.
  • Different operations sharing one key — keys are scoped per body fingerprint, so reuse across distinct payloads is rejected.
  • Long workflows — the 24h TTL is intentional; don't try to use idempotency as a permanent dedupe layer.