Event design
Last updated
Whisperr predicts churn from the events your app and backend send. This page helps you choose those events, name them, and send them so that Whisperr puts each one on the right user’s timeline exactly once.
What to track
Section titled “What to track”Send the events that tell you whether a user will stay. Many of them happen on your backend:
| Signal | Examples |
|---|---|
| Payment health | payment_failed, card_expiring, invoice_overdue |
| Lifecycle | trial_started, trial_expired, subscription_cancelled, plan_downgraded |
| Engagement | report_generated, project_created, checkout_completed |
| Friction | export_failed, support_ticket_opened |
Whisperr stores every valid event, but only registered events drive decisions:
- Before an event list is saved for your app, Whisperr registers each new event name that it receives from an SDK or the ingestion API, up to 50 names, so it can build your plan from the events your app really sends. Review the list in the dashboard before you build the plan.
- After an event list is saved, by you in the dashboard, by your coding agent or by the pull request agent, a new name is stored and listed on the Data page, but it drives no decisions until you add it to the list.
The mobile SDKs also send reserved automatic events: app_installed,
app_updated, app_opened, app_backgrounded, app_error, app_crashed,
screen_viewed, push_opened, push_received and
push_permission_changed. Whisperr knows these names for every app. Do not
send them with track() yourself; that counts them twice.
What the client SDKs capture on their own
Section titled “What the client SDKs capture on their own”From React Native 0.7.0 (with @whisperr/expo 0.2.0), Flutter 0.8.0 and
Swift 0.7.0, the mobile SDKs send these signals with no code from you. They
tell Whisperr how a user came into the app, how much each visit did, and
whether the app failed them.
| Signal | Where it goes |
|---|---|
| Session | context.session_id on every event (sess_ plus a UUID). A new session starts on each cold start and after 30 minutes with no event. |
| Launch source | app_opened.launch_source: push, shortcut, widget, email, universal_link, deep_link or direct. A link adds link_route (the path only, with ids replaced by :id), link_host for http(s) links, and utm_source, utm_medium and utm_campaign when the link has them. A push or email open adds whisperr_message_id. |
| Visit depth | app_backgrounded adds session_id, screen_count, unique_screen_count, event_count (your track() events) and error_count for the visit that ends. |
| Errors | app_error for an uncaught error and for each trackError() call: error_kind, fatal, error_name (the type, such as TypeError), fingerprint, screen_name. Never the message, the stack or a URL. At most one per fingerprint and 20 per session. |
| Crashes | app_crashed on the next launch, before its app_opened, when the last run ended while the app was in the foreground. Debug builds never send it. |
To mark a widget or a quick action, add wsrc=widget or wsrc=shortcut to
the link it opens. Screen counts need screen_viewed events: use the screen
helper of your SDK (React Native,
Expo,
Flutter) or call screen().
captureErrors: false turns off the automatic error capture. trackError()
still sends. Turning off all automatic events also turns off app_error
capture and app_crashed. Errors and crashes are diagnostics data: see
what the SDKs send before you update your App Store and
Google Play answers.
Server SDKs
Section titled “Server SDKs”The server SDKs (Node, Python, PHP, .NET) send no session, launch or error
signals, and have no trackError(): a server error is not what the user saw.
When a failure reaches the user, such as a declined payment or a failed
export, send it as your own event from your backend (payment_failed,
export_failed).
Do not send personal data in event properties. Whisperr drops property keys
that name personal data or secrets, such as email, phone, address, name or
password. See properties.
Naming
Section titled “Naming”Name events in lowercase snake_case that matches
^[a-z0-9]+(?:_[a-z0-9]+)*$. Start with a letter and keep it to 64
characters: Whisperr registers only such names. The Whisperr API trims the name and changes it
to lower case before it checks it, and rejects anything else. Every SDK
checks the name before it queues the event, so one bad name cannot cost you a
batch.
Use object, then verb, in the past tense: payment_failed, plan_upgraded,
subscription_cancelled.
One user, one timeline
Section titled “One user, one timeline”Every event carries an external_user_id: your stable user id, the same
one you use in your own database. Server SDKs take it on every call. The
browser and mobile SDKs fill it in after identify().
Events from before login are not lost. A client SDK either holds them until
identify(), or sends them under an anonymous_id that the next identify
call links to the user. Use the same id everywhere, and events from your app
and your backend merge into one timeline.
Whisperr never messages a user it knows only by anonymous_id.
Timestamps
Section titled “Timestamps”Events carry occurred_at, captured when the event happens, not when it is
sent. Events recorded offline or held in a queue keep their real time.
Whisperr accepts times from 30 days in the past to 5 minutes in the future.
The SDKs send UTC with millisecond precision
(2026-06-14T12:00:00.000Z). Without occurred_at, Whisperr uses the time
it accepted the request.
Duplicates
Section titled “Duplicates”Delivery is at-least-once: a timeout can mean the server got the batch and the
response was lost, so the SDKs retry. Every event carries an idempotency key,
$message_id in context, that stays the same on every retry. Whisperr
stores an event once per $message_id, so retries never create duplicates.
If you call the API directly,
generate $message_id once per event and keep it across retries.
Common questions
Do I have to register an event before I send it to Whisperr?
No. Whisperr stores every valid event. Until an event list is saved for your app, Whisperr also registers each new event name that your app or backend sends with an SDK or the ingestion API, up to 50 names, so it can build your plan from them. Once a list is saved, or after those 50 names, a new name is stored and listed on the Data page, but it drives no decisions until you add it to the list.
Which events do the Whisperr SDKs send on their own?
The mobile SDKs send reserved automatic events such as app_installed, app_opened, app_backgrounded, app_error, app_crashed, screen_viewed and the push events. Whisperr knows these names for every app, so you do not register them.
Can I send the same event from my app and my backend?
Yes, if both use the same external_user_id. Give each event its own $message_id. Two events with different message ids are two events, even when everything else matches.