Sending events

Idempotency

Networks retry. A timeout doesn't tell you whether the event landed, so you resend — and without a guard you'd double-count the order and double-award the badge. An idempotency key makes a resend a safe no-op.

How it works

The pair (tenant, idempotency_key) is unique. The first event with a given key is stored and processed. Any later event with the same key returns the original event and is never processed again — no second ledger entry, no second badge.

You set the key two ways, header first:

  • The Idempotency-Key request header, or
  • An idempotency_key field in the body.

If both are present, the header wins. If neither is set, the event is treated as unique every time.

Same key, sent twice bash
curl -X POST https://api.flyhalf.run/v1/events \
  -H "Authorization: Bearer fh_live_…" \
  -H "Idempotency-Key: order-5512-completed" \
  -d '{ "event_type": "order.completed", "participant": { "external_id": "wc_1042" } }'

first  → 202  {"event_id":"evt_1","status":"received"}
second → 202  {"event_id":"evt_1", ...}   + header  X-Idempotent-Replay: true

A replay returns the same event_id and adds the response header X-Idempotent-Replay: true. That header is how you tell a genuine new acceptance from a deduplicated resend.

Concurrency is handled for you

Two identical requests racing in parallel don't both win — the database's unique constraint lets the first insert through and turns the second into a replay of it. You don't need to serialise retries on your side.

Choosing keys

Use a deterministic key derived from the thing that happened — order-5512-completed, signup-wc_1042. The same real-world event should always produce the same key, so a retry naturally collides. A random UUID per HTTP attempt defeats the whole mechanism.

A fixed quarantine needs a fresh key

Quarantined events (missing event_type or external_id) are stored under their key too. If you correct the body and resend with the same key, you'll just get the quarantined event back as a replay. Cause: the key already exists. Fix: resend the corrected event under a new idempotency key.

Flow actions are idempotent too

You only manage keys at the front door. Inside the engine, every side-effecting flow action derives its own key from {flow_run_id}:{node_id}, so a queue retry mid-flow can't double-award points or re-send a message. The points ledger is append-only and enforces this at the database level. That's internal — but it's why a single event can be safely reprocessed without you thinking about it.