Skip to main content
POST /events reports what a user did. Curious attributes each event to the decisions that preceded it for that user, within the windows of the rewards and constraints that name it; how attribution works is on Events and attribution. Full field reference: the endpoint.

Naming

Names are past tense: purchased, clicked, viewed, signed_up. Use one name with properties rather than many names: email_opened with { "email_name": "welcome" }, not welcome_email_opened, so a reward can match the family or a member of it. Names starting with $ are reserved for the browser SDK’s own system events ($pageview, $click, $form_submit), which the endpoint accepts only from that SDK under a publishable key. Any other caller sending a $ name gets a 400 with `$`-prefixed names are reserved for Curious SDK events; if you forward events from another tool, rename at the source ($pageview to pageview).

value

Optional, defaults to 1; null also means 1. For a semicontinuous or continuous reward it is the amount, in your units. A binary reward ignores it and a count reward counts each event once.

properties versus context

properties describe the event: the plan bought, the email opened, the currency. Rewards and constraints can filter on them. context describes the situation at event time, such as device or page, and is kept with the event but is not a filter. Neither is written to the user; user state changes through decide or identify.

timestamp

When the event happened, as an ISO-8601 datetime with an explicit timezone (2026-04-30T22:33:42Z, 2026-04-30 22:33:42+00:00), or a date alone, read as UTC midnight. A naive datetime such as 2026-04-30 22:33 is a 400 with timestamp must be an ISO-8601 datetime with an explicit timezone. Omit it for events reported as they happen; send it for buffered flushes and backfills, so a late event still attributes to the right decision. A value more than five minutes in the future is rejected as a units or timezone bug. The write time is recorded separately as created_at, which is system-owned: sending it is a 400.

id: idempotent delivery

id is your idempotency key for the event, unique per organization and environment. A retry with the same id is deduplicated and answered as a success with duplicate: true, so a flaky network can never double-count a purchase. Use something stable from your side, such as an order id plus the event name, or let the SDK mint one and keep it across retries. Events without an id are delivered at least once. See Idempotency.

Unknown fields

Events ignore fields they do not know, so an integration can forward a richer record. Two names are the exception and are rejected rather than ignored: created_at, and the retired name as_of (the message says to send the time as timestamp).

The batch form

Up to 200 items. The envelope’s user_id, context and timestamp apply to items that omit them, and items may override them, including the user for cross-user syncs. Results come back per item in input order inside a 200; an item that fails validation gets its own { "status": "error", "error", "message" } and never fails its siblings. See Batch calls.

What makes a good event stream

  • Report the events your rewards and constraints name, with the properties your filters need. Nothing more is required.
  • Report cost-like outcomes too (refunded, unsubscribed); a negatively weighted reward turns them into what Curious avoids.
  • Keep user_id the same id you decide with. If a user starts anonymous and later logs in, identify links the two so earlier events count.
  • Report for everyone, not only users Curious decided for. Attribution finds the decision when there is one.

With the server SDK

See Events and identify with the server SDK. Next: Fallbacks and timeouts. If events arrive but the experiment shows no rewards, see Rewards are not being attributed.