Events: id
Give each event a stable id. A repeated id is deduplicated and answered as a success with duplicate: true; in a batch, the item is { "status": "ok", "duplicate": true } and the envelope’s duplicates counts them. Ids are unique per organization and environment. Events without an id are delivered at least once.
A good id is derived from something stable on your side, so the same fact always produces the same id: <entity id>_<event name>, such as ord_8f3a_purchased or inv_2201_refunded. The server SDK mints sdk-js:<uuid> once per track() call and keeps it across every retry.
Decide: idempotency_key
A decision is a write: it records which arm the user saw, and a second one ends the first’s observation window. So a retried decide must not create a second decision. Send an idempotency_key generated before the first attempt and reused on every retry:
- An opaque string of 1 to 255 characters; a UUID generated per logical request is the recommended shape. It may not contain U+0000 or an unpaired surrogate.
- Keys are scoped to the organization and environment: a test retry never replays a live decision.
- In a batch, the key is per item, and the envelope’s
replayedcounts the replayed items.
“Same request” is judged on the request’s content, not its formatting. Failures are not stored: a
400 under a key leaves the key unclaimed.
Retry matrix
Cap retries at a handful with exponential backoff. The server SDK makes five attempts per event and never retries
decide.
Ambiguous failures
A502 or a dropped connection after the request left you tells you nothing about whether the origin committed. The response’s message says so and names the safe retry: the same idempotency_key for decide, the same id for events. Both make the retry safe either way: if the original committed, you get it back; if it did not, the retry is the first attempt.