Ingesting events
POST /v1/objects/{object_id}/events records one or more events about an object and schedules its analysis.
json
{
"object_type": "user",
"events": [
{ "type": "event", "name": "profile.updated", "metadata": { "field": "bio" } },
{ "type": "event", "name": "payout.requested", "metadata": { "amount": 950, "currency": "usd" } },
{ "type": "message", "content": "is this still available? can I pay by wire?", "occurred_at": "2026-09-27T09:58:00Z" }
]
}| Field | ||
|---|---|---|
events | required | One or more events, below. |
object_type | optional | A classification such as user or listing. |
wait | optional | true scores the object inline instead. See Direct moderation. |
signals | optional | With wait, the signals to score. |
include_history | optional | With wait, whether to score over the object's history (default) or only these events. |
Each event:
| Field | ||
|---|---|---|
type | required | event (an action) or message (text). |
name | optional | The action's name, like login.failed, or a message's subject. |
content | optional | Free text, such as a message body. |
metadata | optional | Any JSON object. |
occurred_at | optional | RFC 3339 time it happened. Defaults to when the API received it. |
The response is 202 Accepted:
json
{ "object_id": "user-42", "accepted": 3, "analysis_status": "scheduled", "analysis_delay_ms": 5000 }Debouncing
The analysis runs analysis_delay_ms after the latest event. Events that arrive in the meantime push it back and join it, so a burst of activity costs one analysis. Send events as they happen; there is no need to batch them yourself, though batching several in one request saves round trips.
What to send
The analyzer reasons over whatever you send, so send the evidence a human moderator would want to see:
- Messages between users, reviews, listing descriptions, support tickets.
- Actions that matter for your signals: sign-ups, profile and payout changes, failed logins, reports by other users, refunds.
- Context in
metadata: amounts, counts, account age, country. Keep it small and relevant; avoid secrets and data you don't need analyzed.
Limits and validation
- A request body is at most 1 MiB.
- Bodies are decoded strictly: an unknown field is a
400, so a typo like"evnts"fails loudly instead of being ignored. typemust beeventormessage, andmetadatamust be valid JSON.- When the prepaid balance is empty, ingestion answers
402and records nothing. See Billing and usage.
Retrying safely
A timeout or a 5xx doesn't tell you whether your events were recorded. Send an Idempotency-Key header, any unique string such as a UUID, and retry with the same key:
bash
curl -X POST "$API/v1/objects/user-42/events" \
-H "Idempotency-Key: 6f1c0e3e-5b8a-4c7e-9f2d-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{"object_type":"user","events":[{"type":"message","content":"hi"}]}'- The events are recorded once, however many times the request is sent.
- A repeat of an asynchronous request is answered like the first and carries
Idempotent-Replayed: true. - A repeat of a
wait: truerequest returns409and does not analyze (or charge) again. Read the verdict withGET /v1/objects/{object_id}. - Using a key for a different object returns
422. - Keys are remembered for 24 hours, per account.
Re-analyzing without new events
POST /v1/objects/{object_id}/analyzeschedules an immediate analysis of one object, for example after changing a signal. It answers404for an object with no events.POST /v1/analyzeschedules every object at bulk priority, behind live traffic. Each analysis is billed, so prefer a backfill when you only added a signal.
Reading what was recorded
GET /v1/objects/{object_id}/events returns the 200 most recent events, newest first, and GET /v1/objects/{object_id}/state returns the compacted summary of older ones.