# Event design

> Decide which product events to send to Whisperr, how to name them, and how Whisperr handles user ids, timestamps and duplicates.

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

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

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](/sdks/react-native/#screens), [Expo](/sdks/expo/#screens-with-expo-router), [Flutter](/sdks/flutter/#screens)) 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](/privacy/sdk-data/) before you update your App Store and Google Play answers.

### 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`](/api/events/#properties-object).

## 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

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

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

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](/api/delivery/#idempotency-message_id), 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.

Source: https://docs.whisperr.net/concepts/events/
