Track events
Last updated
Send product events to Whisperr with POST /v1/events/batch or
POST /v1/events/track. This page lists every field of an event, how
Whisperr validates it, and what the response means.
| Endpoint | Body |
|---|---|
POST /v1/events/batch | { "events": [ <event>, … ] }, 1 to 500 events |
POST /v1/events/track | One <event> |
Both endpoints accept the publishable key (wpk_…) and the server key
(wrk_…). See Which API key do I use?
The event object
Section titled “The event object”{ "external_user_id": "user_8842", "event_type": "payment_failed", "occurred_at": "2026-06-14T12:00:00.000Z", "properties": { "amount_cents": 4900 }, "context": { "$message_id": "f7a1c2e0-4b3d-4f6a-9c1e-8d2b5a7e3f10" }}external_user_id string
Section titled “external_user_id string”Your own stable user id: the same id you use in your database and send to identify. Events from your app and your backend with the same id land on one timeline.
anonymous_id string
Section titled “anonymous_id string”A handle for a visitor you cannot identify yet, 1 to 128 printable
characters. The browser SDK generates a UUID per browser profile. Send it in
place of external_user_id. Each event needs external_user_id,
anonymous_id, or both. When both are present, the event belongs to
external_user_id.
A later identify that carries the same anonymous_id with
an external_user_id moves the anonymous events to that user. Whisperr never
messages an anonymous user.
event_type string, required
Section titled “event_type string, required”Lowercase snake_case that matches ^[a-z0-9]+(?:_[a-z0-9]+)*$. Whisperr
trims the value and changes it to lower case, then checks it. Any other value
is rejected. Validate names before you queue events, so one bad name does not
cost you an event. See Event design for naming guidance.
You cannot send an event name that Whisperr computes itself. Such an event is accepted and then rejected. See Rejections after acceptance.
occurred_at string
Section titled “occurred_at string”When the event happened, as an RFC 3339 timestamp. The SDKs send UTC with
milliseconds and a Z suffix: 2026-06-14T12:00:00.000Z. It must be no more
than 5 minutes in the future and no more than 30 days in the past. Without it,
Whisperr uses the time it accepted the request.
Capture it when the event happens, not when you send it, so buffered and offline events keep their real time.
properties object
Section titled “properties object”Free-form event attributes, up to 50 KB as JSON. An empty value is {},
never [].
Do not send personal data in properties. Whisperr always drops a property
whose key names personal data or a secret, such as an email, phone, address,
name or password key. It does this in context too. It keeps keys that the
event’s registration does not declare, and flags them for 30 days. The
Data page of the dashboard lists dropped keys under Not used →
Personal data removed. The Whisperr MCP tool get_received_events lists dropped and
undeclared keys. Send contact addresses with identify, not
on events.
context object
Section titled “context object”Free-form metadata about the event. Put the idempotency key in
context.$message_id: a unique string per event, up to 200 bytes. A UUID
works. Keep it the same on every retry of the event. Whisperr stores an event
once per $message_id, so retries never create duplicates. Whisperr accepts
an event without $message_id, but cannot deduplicate it. See
Idempotency.
With the publishable key, Whisperr adds context.country: the country of the
request’s IP address, found at the edge. It keeps a country you set
yourself. Server-key events never get it.
user object
Section titled “user object”Optional contact data for the event’s user, with the same rules as identify:
preferred_channel:emailorpush.channels: up to 10 channel objects, as inchannels.
The SDKs do not send user. Use identify unless you must send contact data
with the event.
Send a batch
Section titled “Send a batch”POST /v1/events/batch takes 1 to 500 events. Split a larger backlog into
several requests, each with its own retry state.
curl https://api.whisperr.net/v1/events/batch \ -H "X-API-Key: $WHISPERR_SERVER_KEY" \ -H "Content-Type: application/json" \ -d @- <<EOF{ "events": [ { "external_user_id": "user_8842", "event_type": "payment_failed", "occurred_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "properties": { "amount_cents": 4900 }, "context": { "\$message_id": "$(uuidgen)" } }, { "external_user_id": "user_9917", "event_type": "trial_expired", "occurred_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "properties": {}, "context": { "\$message_id": "$(uuidgen)" } } ]}EOFThe shell fills in the time and a new $message_id for each event. In your
own code, generate $message_id once when the event is created.
Send one event
Section titled “Send one event”POST /v1/events/track takes one event as the whole body.
curl https://api.whisperr.net/v1/events/track \ -H "X-API-Key: $WHISPERR_SERVER_KEY" \ -H "Content-Type: application/json" \ -d @- <<EOF{ "external_user_id": "user_8842", "event_type": "plan_downgraded", "properties": { "from_plan": "pro", "to_plan": "basic" }, "context": { "\$message_id": "$(uuidgen)" }}EOFWhat do the event endpoints return?
Section titled “What do the event endpoints return?”Both endpoints answer 202 Accepted once the request is stored. Whisperr
processes the events right after, normally within seconds.
POST /v1/events/batch:
{ "accepted": 2, "rejected": 0 }rejected counts events that failed validation, such as an unknown field, a
bad event_type or an occurred_at outside the window. Whisperr skips them
and accepts the rest. A problem with the whole request fails it with 400:
invalid JSON, an unknown top-level field, or an empty or oversized events
array.
POST /v1/events/track:
{ "event": { "mapping_status": "pending" }, "processing": "queued" }On track, an event that fails validation is a 400.
Decide what to do from the status code, not from the body. See
Response handling for every status, and
Rejections after acceptance for
the problems that show up only after the 202.
Common questions
Is occurred_at required on a Whisperr event?
No. Without occurred_at, Whisperr uses the time it accepted the request. Send it anyway when the event happened earlier, for example from an offline queue, so the event keeps its real time.
Does Whisperr accept uppercase event names?
Whisperr trims the event_type and changes it to lower case before it checks it, so Payment_Failed is stored as payment_failed. Send lowercase snake_case names: the SDKs reject anything else before they send it.
What happens if I send the same event twice to Whisperr?
Whisperr stores an event once per $message_id. A second event with the same context.$message_id is accepted and ignored. An event without $message_id cannot be deduplicated.
Can I send events before I know who the user is?
Yes. Send anonymous_id instead of external_user_id. When a later identify call carries both the anonymous_id and the external_user_id, Whisperr moves the anonymous events to the identified user. Whisperr never messages an anonymous user.