Skip to content

Track events

Record product events — churn signals — for a user.

Endpoint Body
POST /v1/events/batch { "events": [ <event>, … ] } — ≤ 500 events
POST /v1/events/track a single <event>
{
"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 pass to identify. Frontend and backend events with the same id land on one timeline.

Lowercase snake_case, matching ^[a-z0-9]+(?:_[a-z0-9]+)*$. The server rejects anything else. Validate before sending so one bad name can’t poison an otherwise-valid batch. See Event design for naming guidance.

When the event happened — RFC3339 UTC with millisecond precision and a Z suffix (2026-06-14T12:00:00.000Z). Must be within +5 minutes / −30 days of now. Capture it at event time, not send time, so buffered or offline events keep their real time.

Free-form event attributes. An empty value serializes as {}, never [].

Free-form metadata. $message_id is required: a per-event idempotency key — any stable unique string (UUID recommended). It must stay identical across retries of the same event so the server can deduplicate at-least-once delivery. See Delivery, retries & idempotency.

Terminal window
curl -X POST https://api.whisperr.net/v1/events/batch \
-H "Content-Type: application/json" \
-H "X-API-Key: $WHISPERR_API_KEY" \
-d '{
"events": [
{
"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-…" }
},
{
"external_user_id": "user_9917",
"event_type": "trial_expired",
"occurred_at": "2026-06-14T12:00:01.250Z",
"properties": {},
"context": { "$message_id": "0b6d9e44-…" }
}
]
}'

Batches are capped at 500 events. Split larger backlogs into multiple requests, each with its own retry state.