Events

Ingest usage events

Changed on

Send up to 100 usage events in one request.

Returns 202 Accepted with the number of events accepted and the event_id of each, in submission order — your own value where you supplied one, a generated value where you did not.

Ingestion is idempotent on (store, event_id): re-sending an event_id this store has already accepted records nothing and is not an error, so retrying a whole batch after a timeout or a partial failure is always safe. Duplicates are deduplicated silently rather than reported — a replayed batch returns the same 202 and the same event_ids as the original. Supply your own event_id to get that guarantee; when you omit it we generate one, which makes the event unique and a retry a second event.

Your event_id is stored trimmed, and the event_ids we return are the stored values — so " abc " is recorded and echoed as "abc", which is what reference= on GET /v1/events matches. Two entries in one batch whose ids differ only by surrounding whitespace are therefore the same event: the second is deduplicated against the first, both are reported accepted, and both carry the same id. An event_id that is entirely whitespace is rejected like an empty one (422) rather than being replaced with a generated id — an unusable key is worth telling you about.

The whole batch is validated before anything is accepted: if any event is invalid the request is rejected with 422 and param pointing at the offending event (e.g. events[3].customer_id), and no event in the batch is ingested.

Billing attribution uses the timestamp you supply (defaulting to the time of ingestion), bounded by the late-event window: once a billing period has been closed for longer than that window, an event arriving for it is still stored durably but is no longer folded into that period's totals.

202, not 200, is deliberate. It means the batch has been accepted for processing, not that every downstream effect has completed: aggregation into meter totals is already asynchronous today, and the transport behind this endpoint may become queue-backed. A queue producer cannot know how many rows a consumer will ultimately insert, so this response reports acceptance rather than an insert/duplicate split — the contract you code against stays identical when that lands.

post/v1/events/ingest

Request

Request body

Example request

{
  "events": [
    {
      "name": "image.generated",
      "customer_id": "cust_abc123",
      "external_customer_id": "user-42",
      "event_id": "order-1234",
      "timestamp": "2026-08-15T10:30:00.000Z",
      "properties": {
        "tokens": 1200,
        "tier": "pro"
      }
    }
  ]
}

Response

Batch accepted for processing

acceptednumber required

How many events were accepted. Always the size of the submitted batch — validation is all-or-nothing, so a 202 means every event in the batch was accepted.

event_idsstring[] required

The event_id each accepted event was recorded under, in the order they were submitted — your own value where you supplied one, the generated value where you did not. This is the stored id after normalisation (surrounding whitespace is trimmed), so it is exactly what reference on GET /v1/events will match. Use it to correlate a submitted event with the row it became.

Example response

{
  "accepted": 100,
  "event_ids": [
    "order-1",
    "order-2"
  ],
  "warnings": [
    {
      "index": 3,
      "code": "no_matching_meter"
    }
  ]
}

Changes