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-Keyrequest header, or - An
idempotency_keyfield in the body.
If both are present, the header wins. If neither is set, the event is treated as unique every time.
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.