Skip to main content

Idempotency

A notification you send twice is worse than one you send late. Send an Idempotency-Key and Nervly guarantees that a replay returns the original response instead of sending again.

Without a key, every call sends​

This is the default. If your HTTP client times out after the gateway has already published the event, your retry produces a second notification. There is no server-side deduplication on payload content.

With a key​

Send the Idempotency-Key header with your unique business identifier. Each example below generates an RFC 4122 version 4 UUID and attaches it to a POST /v1/events/trigger:

IDEMPOTENCY_KEY="$(uuidgen | tr 'A-Z' 'a-z')" # RFC 4122 version 4

curl -X POST https://api.nervly.io/v1/events/trigger \
--connect-timeout 5 \
--max-time 10 \
-H "Authorization: Bearer $NERVLY_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "payment-received", "to": { "subscriberId": "user_8f21c", "phone": "+2348012345678" } }'
CallStatusBody
First202 Acceptedfresh eventId, status: "QUEUED"
Replay with same key200 OKthe original body, byte for byte

The status code is how you tell them apart. The replayed body still says "status": "QUEUED" and still carries the original eventId and timestamp — that is the point. Your code sees one event, not two.

Choosing a key​

Derive the key from your own domain, so it is the same across every retry of the same logical action:

// Good — stable across retries, unique per real event
const key = `invoice-${invoice.id}-paid`;
const key = `order-${order.id}-shipped-attempt-${order.shipmentCount}`;

// Useless — a new key every attempt defeats the whole mechanism
const key = crypto.randomUUID();

If you cannot derive one, generate the UUID once, store it with the job, and reuse it on every retry of that job.

The retention window​

Keys are held in Redis with a TTL set by the gateway's IDEMPOTENCY_TTL_SECONDS, 24 hours by default.

The published contract says 48 hours

info/api_schema_contract.md documents a 48-hour window. The gateway's compiled default is 24 hours (IDEMPOTENCY_TTL_SECONDS=86400). The running configuration wins — confirm the deployed value before relying on a window longer than 24 hours.

After the TTL expires, the key is forgotten and a request using it is treated as new. Size your retry windows inside the TTL.

What happens when Redis is unavailable​

Idempotency degrades open, not closed. A Redis failure during the lookup is indistinguishable from a cache miss: the request proceeds and the notification is sent. A Redis failure during the write is swallowed, so the key is not recorded and a later replay will send again.

Idempotency is best-effort, not transactional

This is a deliberate trade-off — a Redis outage degrades duplicate protection rather than taking ingestion down. It also means idempotency is not a substitute for your own deduplication when duplicates are genuinely unacceptable. For money-movement notifications, keep a sent-flag in your own database.

Conflicts​

The error contract defines 409 IDEMPOTENCY_CONFLICT for a key reused with a different payload. The gateway does not currently compare payloads — a reused key returns the cached response regardless of what body you send. So reusing a key for a genuinely different notification silently drops the second one rather than erroring.

Keep keys unique per logical notification and this cannot bite you.

Next​