Event Ingest

Use the event ingest API for frequent, small product-event batches such as page views, clicks, signups, and tracking data from an internal app or website.

To write arbitrary real columns into any lakehouse table, or to upsert / replace rows, use Stream Ingest instead. Event Ingest is append-only into the fixed product-event schema below.

There are three write paths:

  • Server-side writers use a normal Definite session token or API token with POST /api/v1/events/{schema}/{table}.
  • Browser/client-side writers use a public event write key (evpub_...) with POST /api/v1/events/collect.
  • SaaS webhook senders that can only set a URL use a server-kind event source: the same evpub_... key, passed as a ?write_key= query parameter on POST /api/v1/events/collect.

Do not put a normal Definite API token in browser code. Public event write keys are safe to expose because they are append-only, scoped to one destination table, limited by an origin allowlist (for browser sources), and can be rotated or revoked.

Event Table Shape

Both endpoints append rows directly to a DuckLake table:

POST /api/v1/events/{schema}/{table}
Authorization: Bearer <token>

The destination table is created if it does not exist. The table shape is stable so changing event properties do not require migrations:

ColumnTypeNotes
event_idVARCHARCaller-provided event_id or id; generated when omitted.
event_timeTIMESTAMPTZCaller-provided event_time or timestamp; ingestion time when omitted.
event_nameVARCHARRequired. Also accepts event or name.
distinct_idVARCHAROptional. Also accepts user_id or anonymous_id.
propertiesVARCHARJSON string containing event properties and extra top-level fields.
ingested_atTIMESTAMPTZServer ingestion time.

The destination table is created if it does not exist.

Server-Side Writers

Use Authorization: Bearer <token> where <token> is a normal Definite session token or API token. This endpoint is for trusted servers, scripts, and backend collectors.

Send a JSON array for normal batching:

curl -sS "$DEFINITE_API_URL/api/v1/events/raw/events" \
  -H "Authorization: Bearer $DEFINITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "event_id": "evt_001",
      "event_name": "pageview",
      "event_time": "2026-05-20T14:30:00Z",
      "distinct_id": "anon_123",
      "properties": {
        "path": "/pricing",
        "utm_source": "newsletter"
      }
    }
  ]'

The API also accepts a single JSON object or an envelope shaped as {"events": [...]}.

NDJSON

Use NDJSON when a collector already buffers one event per line:

curl -sS "$DEFINITE_API_URL/api/v1/events/raw/clicks" \
  -H "Authorization: Bearer $DEFINITE_TOKEN" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary $'{"event":"click","user_id":"u_1","properties":{"button":"buy"}}\n{"event":"scroll","user_id":"u_1","properties":{"depth":75}}\n'

Browser Writers

Create a public event source in Settings -> Event Sources, or with the CLI:

definite run event-source create \
  --name "Marketing site" \
  --schema raw \
  --table events \
  --origin https://www.example.com

The create and rotate commands print the full evpub_... key once. Store it in the client app configuration for that allowed origin. The settings page keeps only the display prefix after the key is dismissed.

Send browser events to the fixed collector endpoint:

await fetch('https://definite.example.com/api/v1/events/collect', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer evpub_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    events: [
      {
        event: 'pageview',
        distinct_id: 'anon_123',
        properties: { path: location.pathname },
      },
    ],
  }),
})

For navigator.sendBeacon, put the write key in the JSON envelope because beacon calls cannot set the Authorization header:

navigator.sendBeacon(
  'https://definite.example.com/api/v1/events/collect',
  new Blob([
    JSON.stringify({
      write_key: 'evpub_...',
      events: [{ event: 'pagehide', properties: { path: location.pathname } }],
    }),
  ], { type: 'text/plain' }),
)

The API checks the browser Origin header against the event source allowlist before writing. Add every exact application origin that will send events, such as https://www.example.com and https://app.example.com. Wildcards are not accepted.

Manage sources from Settings or the CLI:

definite run event-source list
definite run event-source rotate <source_id>
definite run event-source revoke <source_id>

Server Webhook Writers

Use a server-kind event source when a SaaS product pushes webhooks and the only thing you control is the destination URL: no custom headers, no body shape. Read.ai meeting-transcript webhooks are the canonical example.

Create the source with the CLI:

definite run event-source create \
  --name "Read.ai webhooks" \
  --schema readai \
  --table raw_meetings \
  --kind server \
  --default-event-name meeting_end \
  --max-payload-bytes 8388608

--origin is not required for server sources. Origins are meaningless here: server-to-server posts carry no Origin header, so the Origin allowlist check is skipped entirely for server-kind sources. Browser-kind sources still hard-require an allowlisted Origin.

Paste the collector URL with the write key as a query parameter into the SaaS product's webhook configuration:

https://definite.example.com/api/v1/events/collect?write_key=evpub_...

The collector accepts the write key from the Authorization header, the JSON body write_key field, or the write_key query parameter, in that order of precedence. A webhook post needs nothing beyond the URL:

curl -X POST 'https://definite.example.com/api/v1/events/collect?write_key=evpub_...' \
  -H 'Content-Type: application/json' \
  -d '{"session_id":"abc","trigger":"meeting_end","title":"Weekly sync","transcript":{"speaker_blocks":[]}}'

Default event name

Webhook payloads arrive in the sender's own shape and usually have no event_name, event, or name key. Set --default-event-name on the source and it is applied to any incoming event that lacks one; events that do carry a name keep it. Without a source default, an event with no name is still rejected with a 400. All other fields land in the properties JSON column as usual.

Payload limits for server sources

Server sources are admin-created and trusted, so the source's own max_payload_bytes governs, capped by a hard 32 MiB ceiling, instead of being limited by the global events.max_payload_bytes deployment setting. This lets a single deployment accept large webhook payloads (meeting transcripts run to several MB) on one source without raising the global limit for every writer. max_batch_rows keeps the min(source, global) behavior for both kinds.

Key placement in the URL

Putting the write key in a URL is acceptable for this use case: the key is append-only, scoped to a single destination table, and can be rotated (definite run event-source rotate <source_id>) or revoked at any time. It cannot read, query, or administer anything. If a webhook URL leaks, rotate the key and update the sender's configuration.

Limits and Inlining

Event ingest is intentionally bounded for small, frequent batches:

ConfigDefaultMeaning
events.max_batch_rows1000Maximum events accepted in one request.
events.max_payload_bytes1048576Maximum request body size.
lakehouse.data_inlining_row_limit1000DuckLake row threshold for inlining small writes.

Browser event sources can set lower per-source max_batch_rows and max_payload_bytes limits. The effective limit is the lower of the global deployment setting and the source-specific setting. The exception is max_payload_bytes on server-kind sources, where the per-source value governs up to a hard 32 MiB ceiling (see "Server Webhook Writers" above).

Keep events.max_batch_rows less than or equal to lakehouse.data_inlining_row_limit when event writes should stay in DuckLake's metadata catalog instead of producing tiny Parquet files. Set lakehouse.data_inlining_row_limit: 0 to disable DuckLake inlining.

Flushing Inlined Data

Inlined rows are queryable immediately. To materialize them into Parquet files, run the maintenance operation:

# Flush all inlined rows in the lake.
definite run maintenance run --operation flush_inlined_data

# Flush one event schema.
definite run maintenance run --operation flush_inlined_data --schema raw

# Flush one event table.
definite run maintenance run --operation flush_inlined_data --schema raw --table events

checkpoint also flushes inlined data as part of DuckLake maintenance, but the targeted flush_inlined_data operation is usually easier to schedule for event tables.