Skip to content

Events API

Ingest event batches with sdk.events.v1.EventsService/BatchCreate - full request schema, property wrapping and validation rules.

Updated View as Markdown

Ingest events from SDKs and server-side integrations using sdk.events.v1.EventsService/BatchCreate.

Auth: x-api-key header (public key pub_... or private key prv_...). No Authorization header or x-project-id is required - the project is resolved from the key. See Authentication.

BatchCreate

POST /sdk.events.v1.EventsService/BatchCreate

Validates and enqueues up to 1 000 events in a single request. Returns the count of accepted events. There is no single-event endpoint - all ingestion goes through this method.

All google.protobuf.Timestamp fields (here, occurTime, and timestampValue inside a PropertyValue) serialize as RFC 3339 strings - e.g. "2026-06-05T10:00:00Z".

Event fields

Proto field JSON name Type Required Notes
event_id eventId string Yes UUID (any version; UUIDv7 recommended for time-sortability)
auto_properties autoProperties map<string, PropertyValue> No Context set by the SDK or server. Keys must start with $. $bot_score and $verified_bot are server-only - any client-supplied value is stripped and replaced from CDN headers
custom_properties customProperties map<string, PropertyValue> No Your event properties. Keys must not start with $
distinct_id distinctId string Unless cookieless Anonymous or identified user ID. Must not start with cookieless- - that prefix is server-owned
kind kind string Yes Event name, matching ^[a-zA-Z0-9_.-]+$. Must not start with pug. (reserved prefix)
occur_time occurTime google.protobuf.Timestamp -> RFC 3339 Yes When the event occurred
session_id sessionId string Unless cookieless UUID grouping events in a visit
cookieless cookieless bool No Ask the server to derive identity instead of sending it. See Cookieless events

Proto field names (snake_case) and lowerCamelCase are both accepted by the Connect JSON codec. The lowerCamelCase forms shown above are canonical.

Cookieless events

Set "cookieless": true when the client has stored no identifier - the Web SDK’s default consent state, and the shape to send from any client that hasn’t obtained consent. The server then derives an ephemeral, daily-rotating distinctId and stitches a sessionId (closed after 30 minutes of inactivity), so the traffic still counts while staying anonymous.

{
  "eventId": "01966b9e-1234-7abc-abcd-0123456789ab",
  "kind": "page_view",
  "cookieless": true,
  "occurTime": "2026-06-05T10:00:00Z",
  "customProperties": {},
  "autoProperties": {}
}

The identity fields must be omitted entirely, not sent empty: a set sessionId has to satisfy the UUID rule, so "" is rejected. A batch may mix cookieless and identified events freely.

Derived IDs carry a cookieless- prefix, which is why a client may never send one. They are excluded from user-counting metrics by default, so a project running cookieless sees visitor counts without inflated user counts.

PropertyValue: typed property wrapper

Every value in customProperties and autoProperties is a wrapped object that names its type explicitly. This is the Connect JSON encoding of the protobuf oneof.

Variant JSON shape Notes
String { "stringValue": "hello" } Max 1 024 Unicode code points. Values exceeding the limit are rejected with invalid_argument (not truncated)
Integer { "intValue": "42" } int64 - the value is a JSON string, not a number
Double { "doubleValue": 3.14 } Must be finite (NaN and +/-Infinity are rejected)
Boolean { "boolValue": true }
Timestamp { "timestampValue": "2026-06-05T10:00:00Z" } RFC 3339; stored at millisecond precision

Exactly one variant must be set per PropertyValue object. Proto snake_case aliases (e.g. string_value) are accepted as well.

Validation summary

All rules above enforced together; any violation fails the request with invalid_argument:

  • Batch size: at most 1 000 events per BatchCreate call.
  • eventId, sessionId: must be valid UUIDs.
  • Identity: distinctId and sessionId are required unless cookieless is true, and must be omitted entirely when it is.
  • cookieless- prefix: server-owned; a client-sent distinctId starting with it is rejected (as is an externalId on Identify).
  • $bot_score, $verified_bot: server-only; any client-supplied value is dropped and re-set from the CDN-injected CF-Bot-Score/CF-Verified-Bot headers.

(Property-key prefix rules, stringValue length, and doubleValue finiteness are covered in the Event fields and PropertyValue tables above.)

Request / response shape

Property values are typed PropertyValue wrappers. Every value in customProperties and autoProperties names its type explicitly: intValue (int64) must be a JSON string, doubles are bare numbers, etc. (see the table above). This is not how profile traits work: Identify’s traits are plain JSON, not PropertyValue-wrapped; see Profiles.

BatchCreateRequest

{
  "events": [
    {
      "eventId":   "<uuid>",
      "distinctId": "anon-abc123",
      "kind":       "page_view",
      "sessionId":  "<uuid>",
      "occurTime":  "2026-06-05T10:00:00Z",
      "customProperties": {
        "plan":  { "stringValue": "pro" },
        "seats": { "intValue": "5" },
        "mrr":   { "doubleValue": 299.0 },
        "trial": { "boolValue": false }
      },
      "autoProperties": {}
    }
  ]
}

BatchCreateResponse

{ "accepted": 1 }

accepted (uint32) is the number of events that passed validation and were enqueued. It equals the number of events in the request when all events are valid.

Two more fields appear when the server refuses part of a batch while resolving cookieless identity - accepted plus dropped always equals the number sent:

{ "accepted": 8, "dropped": 2, "droppedByReason": { "salt_unavailable": 2 } }

A partial drop is deliberately not an error: a batch may legitimately mix cookieless and identified events, so failing the whole request would discard healthy traffic to report a fault affecting part of it. That makes dropped the only signal that anything was lost - check it rather than assuming a 200 means everything landed.

Reason Cause Retry the same payload?
day_out_of_range occurTime fell outside the today/yesterday UTC salt window (usually severe client clock skew) No - it drops again
salt_unavailable The daily salt could not be read or minted, so identity can’t be derived (and is never fabricated). Server-side Yes
salt_corrupt The day’s stored salt could not be decoded. Nothing overwrites it, so it re-rejects until the key expires No - needs operator intervention

A fourth reason, identity_headers_missing (no User-Agent, or no resolvable client address - usually a proxy stripping headers), rejects the whole request instead, so it surfaces as an error rather than in droppedByReason.

Errors

These are the codes BatchCreate returns in practice - a subset of the full error model:

Connect code HTTP status Cause
unauthenticated 401 Missing or invalid x-api-key
invalid_argument 400 Validation failed: see the message field for which rule
resource_exhausted 429 Rate limit exceeded
internal 500 Server error: retry with exponential backoff

Error responses follow the Connect envelope:

{
  "code": "invalid_argument",
  "message": "event kind must not use reserved prefix 'pug.'"
}

Further reading

Navigation

Type to search...

↑↓ navigate↵ selectEsc close