Skip to content

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.

EndpointBody
POST /v1/events/batch{ "events": [ <event>, … ] }, 1 to 500 events
POST /v1/events/trackOne <event>

Both endpoints accept the publishable key (wpk_…) and the server key (wrk_…). See Which API key do I use?

{
"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" }
}

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.

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.

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.

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.

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.

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.

Optional contact data for the event’s user, with the same rules as identify:

  • preferred_channel: email or push.
  • channels: up to 10 channel objects, as in channels.

The SDKs do not send user. Use identify unless you must send contact data with the event.

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)" }
}
]
}
EOF

The 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.

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)" }
}
EOF

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.