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_...) withPOST /api/v1/events/collect. - SaaS webhook senders that can only set a URL use a
server-kind event source: the sameevpub_...key, passed as a?write_key=query parameter onPOST /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:
| Column | Type | Notes |
|---|---|---|
event_id | VARCHAR | Caller-provided event_id or id; generated when omitted. |
event_time | TIMESTAMPTZ | Caller-provided event_time or timestamp; ingestion time when omitted. |
event_name | VARCHAR | Required. Also accepts event or name. |
distinct_id | VARCHAR | Optional. Also accepts user_id or anonymous_id. |
properties | VARCHAR | JSON string containing event properties and extra top-level fields. |
ingested_at | TIMESTAMPTZ | Server 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:
| Config | Default | Meaning |
|---|---|---|
events.max_batch_rows | 1000 | Maximum events accepted in one request. |
events.max_payload_bytes | 1048576 | Maximum request body size. |
lakehouse.data_inlining_row_limit | 1000 | DuckLake 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.