Delivery, retries and idempotency
Last updated
Whisperr ingestion is at-least-once: a timeout can mean that the server stored your request and the response was lost. This page says how to handle each response from the ingestion API and how to retry without duplicates. Every Whisperr SDK does this already. If you call the API directly, do the same.
Accepted, then processed
Section titled “Accepted, then processed”The Whisperr API checks the shape of each request: valid JSON, known fields,
sizes, snake_case event names and the occurred_at window. It then stores
the request and answers 202 Accepted. Resolving the user, applying traits
and channels, and evaluating the event happen right after, normally within a
few seconds.
So a 2xx means Whisperr has your data and will process it, not that it
has already processed it. Once you get one, the request is Whisperr’s
responsibility: remove it from your queue and do not resend it.
Whisperr applies requests per app in the order it accepted them. An
identify sent before a track is applied before it. That is why a
visitor’s anonymous events are always attached when you later identify them.
Send identify through the same queue as your events, not around it.
Response handling
Section titled “Response handling”| Response | Class | What to do |
|---|---|---|
2xx | ok | Accepted. Remove the request from your queue. |
401, 403 | auth | The key is wrong, revoked, or the wrong kind. Stop sending, report the error, and keep the request queued. Retrying does not help until the key is fixed. |
429, 5xx, network error, timeout | retry | Temporary. Retry with bounded exponential backoff. When retries run out, keep the request queued for a later attempt. Do not drop it. |
Any other 4xx, such as 400 or 413 | drop | The request is malformed and will never succeed. Drop it and log it. This is a bug in your integration, not a temporary fault. |
Decide on the status class, not on an exact status code or on fields in the response body.
Two rules follow from the table:
- Auth errors and exhausted retries keep the data. Malformed requests drop
it. Temporary and auth failures can recover, so the data stays queued. A
400fails the same way every time, so a retry only uses up your rate limit. - A malformed event does not sink its batch. An event with an unknown
field, a bad
event_typeor anoccurred_atoutside the window is counted in the batch response’srejectedand skipped. The rest are accepted. Only a problem with the whole request fails the batch with400: invalid JSON, an unknown top-level field, or an empty or oversizedeventsarray. Validate events before they enter your queue anyway: a rejected event is gone.
Rejections after acceptance
Section titled “Rejections after acceptance”A few problems show up only while Whisperr processes a request, after the
202. They do not come back in the response. Whisperr sets the affected event
or identify aside and processes the rest of the batch normally, so your queue
keeps flowing:
- Event names that Whisperr computes (
derived_event_code): theevent_typeis an event Whisperr derives itself, so you cannot send it. - Contact changes from the publishable key
(
contact_change_requires_server_key): giving a user a different email address than the stored one needs the server key.
Fix these at the source. Resending the same request fails the same way.
To see them, open the Data page of the Whisperr dashboard and check the
Not used section. It shows the last 24 hours of rejected requests and of
events Whisperr could not apply, by reason. Coding agents can read the same
data with the Whisperr MCP tools list_ingest_dead_letters and
get_ingest_rejects (see Use with your AI agent).
Why does Whisperr ignore bot traffic?
Section titled “Why does Whisperr ignore bot traffic?”Whisperr drops events and identify calls that come from known crawlers,
headless browsers, link previewers and AI agents. For the publishable key it
checks the request’s User-Agent header. For every key it checks the
user_agent or userAgent in the event’s context. The response is still
202, so SDKs do not retry. It carries an X-Whisperr-Filtered header, such
as bot; class=headless_browser; count=2. The Data page shows the count
under Not used → Bots ignored.
If you test with Playwright or Puppeteer in headless mode, the events are dropped.
Idempotency: $message_id
Section titled “Idempotency: $message_id”Put a per-event idempotency key in each event’s context.$message_id: any
stable unique string up to 200 bytes. A UUID works. Whisperr stores an event
once per $message_id, which makes at-least-once retries safe. Whisperr
accepts an event without $message_id, but cannot deduplicate it.
The rule: generate $message_id once, when the event is created, never
per attempt. Every retry of an event must send the same id. If you create a
new id on retry, every timeout can become a duplicate event.
create event ──▶ assign $message_id ──▶ enqueue │ send ◀── retry ◀─────┤ same $message_id every attempt │ 2xx ──▶ dequeueBackoff
Section titled “Backoff”Use bounded exponential backoff for the retry class (429, 5xx, network
errors). When a 429 or 503 carries Retry-After, wait at least that
long before the next attempt, up to 60 seconds. The Node SDK, for example,
retries up to 6 times, with a 10-second timeout per request. After the last attempt,
keep the request and try again on your next flush instead of dropping it.
Conformance
Section titled “Conformance”The whisperr-spec repository pins this contract with executable fixtures:
conformance/wire.json for request bodies and conformance/behavior.json
for the retry, drop and keep outcomes. Every official SDK runs them in CI. If
you build your own client, test it against them.
Common questions
Does a 202 from Whisperr mean the event was processed?
No. 202 means Whisperr stored the request and will process it, normally within seconds. Remove the request from your queue and do not send it again.
Should I retry a 400 from the Whisperr API?
No. A 400 means the request is malformed, and the same bytes fail the same way every time. Drop it, log it and fix the integration.
Why are my events missing although the API answered 202?
Either the request was rejected during processing, or it came from a known bot or crawler user agent. Open the Data page of the Whisperr dashboard and check the Not used section, or call the Whisperr MCP tool get_received_events.
How long should I wait after a 429 from Whisperr?
Wait the number of seconds in the Retry-After header, then retry. The SDKs cap that wait at 60 seconds.