Events
An event records that something in your team changed, and carries a snapshot of the object as it stood immediately afterwards. Together they form the team's change feed: a time-ordered, resumable log you can poll, replay, or have pushed to you as webhooks.
events:read is not in a key's default ability set — grant it explicitly
when minting the key. See authentication.
Events or the audit log?
Both are chronological logs of team activity, and they are not interchangeable. Reach for the wrong one and you will either miss changes or drown in reads.
/v1/events | /v1/audit_events | |
|---|---|---|
| Answers | What changed | Who did what, reads included |
| Purpose | Driving integrations | Compliance and forensics |
| Per-entry id | Yes (evt_…), stable and deduplicable | No — entries are addressed by log position |
| Reads recorded | Never | Yes, every passport read |
| Retention | Fixed at 30 days for every team | Configured per team by admins |
| Delivered by webhook | Yes | No |
| Payload | Full object snapshot | The change context (old/new values) |
The short version: if you are building software that reacts to changes, use events. If you are answering "who looked at this passport in March", use the audit log. Neither is a superset of the other — events never record reads, and audit entries have no id to resume or deduplicate on.
The event model
- Name
id- Type
- string
- Description
The
evt_identifier. K-sortable, and the deduplication key for webhook consumers.
- Name
object- Type
- string
- Description
Always
event.
- Name
type- Type
- string
- Description
A dotted type from the catalog.
- Name
created- Type
- string
- Description
When the change was recorded, ISO 8601 UTC.
- Name
data- Type
- object
- Description
Carries
object, the snapshot of the subject, and — only when the snapshot was too large —truncated: true.
- Name
request_id- Type
- string
- Description
The
req_id of the API call that caused the change, or null for background work.
- Name
actor- Type
- object
- Description
{ user_id, api_key_id }. Either may be null; on two event types both are. See events with no actor.
These are the exact bytes a webhook delivery POSTs. An event fetched from GET /v1/events/{event} and the same event received on your webhook endpoint are byte-identical, which is what lets you build against the feed first and turn on delivery later.
Snapshots
data.object is produced by the same code that serves the object's own GET, so it is byte-identical to what that endpoint would have returned at the moment of emission. A passport.updated event carries the whole passport, not a diff — you rarely need a follow-up call to find out what the object now looks like.
Two consequences worth internalising. First, the snapshot is a point-in-time value and the object may have changed again since; if you need current truth, re-fetch. Second, the snapshot reflects the object as the emitting code saw it — lab.enabled and lab.disabled have no read endpoint behind them and carry a purpose-built payload instead, {object, team, enabled, enabled_at}.
audit_settings.updated and design.updated are the other side of that coin. Both do have a read endpoint behind them, so their snapshots are the ordinary kind: byte-identical to what GET /v1/audit_settings and GET /v1/design would have returned at that moment.
api_key.revoked is a third case: its snapshot is the ordinary kind, but the row it describes no longer exists — so re-fetching returns 404 and the snapshot is the only record. All three api_key.* payloads omit the key's secret, which the key resource structurally cannot render.
Truncated snapshots
A snapshot over 64 KB is not dropped; it degrades. data.object becomes a thin reference carrying only id and object, and data.truncated appears set to true:
Truncated event
{
"type": "passport.updated",
"data": {
"object": { "object": "passport", "id": "pass_3e9a1c5b7d2f4083" },
"truncated": true
}
}
Treat the presence of data.truncated as the instruction to re-fetch the object from its own endpoint. It is absent — not false — on a normal event, so test for presence rather than for a truthy value. A consumer that reads data.object fields blindly will silently see nulls on large objects, so this is worth handling on day one even if your objects are small today.
One event per call
Events are emitted once per API call, not once per changed field. A single PATCH /v1/passports/{passport} that changes property values, friendly_id, publish_status and metadata produces exactly one passport.updated, carrying the final state after all four changes.
Three related rules complete the picture:
- Reads never emit. Retrieving a passport is recorded in the audit log and nowhere else. Nothing you do with a
GETwill ever appear here. - Mass imports emit batch-level events only. A 50,000-row import produces
import.startedand one terminal event — never 50,000passport.createdevents. If you need per-row detail, walkGET /v1/imports/{import}/rows. - Idempotency replays never re-emit. A replayed
POSTreturns the stored response without re-executing, so it cannot produce a second event. See idempotency.
Retention
Events are kept for 30 days, then pruned. This is a platform constant, not a team setting — deliberately, because a webhook replay window has to mean the same thing for every consumer and every integration built against the API.
A pruned event returns 404 from GET /v1/events/{event}. A webhook delivery record can outlive the event it refers to, which is one reason a consumer reconciles by re-fetching the object rather than the event.
If your integration can be down for longer than 30 days, the feed is not your recovery mechanism — reconcile by listing the objects themselves.
List events
The team's change feed, newest first. Requires events:read. Returns a paginated list.
An unknown value for type or types[] is rejected with 400 parameter_invalid rather than returning an empty page, so a typo in a poller surfaces immediately instead of looking like silence.
Every filter joins the cursor's scope: a cursor minted under one set of filters is rejected with 400 invalid_cursor if replayed under another. Keep the filters identical for the whole walk and vary only limit.
Optional attributes
- Name
type- Type
- string
- Description
A single event type from the catalog.
- Name
types[]- Type
- array
- Description
Several types; repeat the parameter per value. Combines with
typeas a union, not an intersection.
- Name
object_id- Type
- string
- Description
Only events whose subject is this object — a passport, template, ontology, composition, import, API key or Lab run id.
- Name
created[gte]- Type
- string
- Description
Only events at or after this time. Unix seconds or an ISO date, inclusive of the whole second named.
- Name
created[lte]- Type
- string
- Description
Only events at or before this time.
- Name
limit- Type
- integer
- Description
Page size, 1–100 (default 10).
- Name
cursor- Type
- string
- Description
The previous response's
next_cursor, verbatim.
Request
curl -G https://api.synexcloud.com/v1/events \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-d "types[]=passport.created" \
-d "types[]=passport.updated" \
-d "created[gte]=2026-07-01" \
-d "limit=20"
Response
{
"object": "list",
"data": [
{
"id": "evt_2c7f9a4e1b8d3506",
"object": "event",
"type": "passport.updated",
"created": "2026-07-22T14:18:02Z",
"data": {
"object": {
"object": "passport",
"id": "pass_3e9a1c5b7d2f4083",
"friendly_id": "PACK-24-0917",
"values": { "property_capacity": { "value": 78.4 } }
}
},
"request_id": "req_0a9f3c2b1e4d7f0a",
"actor": { "user_id": 118, "api_key_id": 42 }
}
],
"has_more": true,
"next_cursor": "evt_2c7f9a4e1b8d3506",
"url": "/v1/events"
}
Retrieve an event
Reads one event by its evt_ id. Requires events:read.
An event outside the 30-day retention window is 404 resource_missing.
Request
curl https://api.synexcloud.com/v1/events/evt_2c7f9a4e1b8d3506 \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"id": "evt_2c7f9a4e1b8d3506",
"object": "event",
"type": "passport.updated",
"created": "2026-07-22T14:18:02Z",
"data": {
"object": {
"object": "passport",
"id": "pass_3e9a1c5b7d2f4083",
"friendly_id": "PACK-24-0917",
"values": { "property_capacity": { "value": 78.4 } }
}
},
"request_id": "req_0a9f3c2b1e4d7f0a",
"actor": { "user_id": 118, "api_key_id": 42 }
}
A resumable poller
Because ids are k-sortable and the feed is ordered on them, the last event id you processed is a durable high-water mark. Poll newest-first, stop when you reach an id you have already seen, and persist the newest id of the batch:
GET /v1/events?limit=100with your filters.- Process rows until you hit your stored high-water mark id.
- If you did not reach it and
has_moreis true, follownext_cursor. - Store the newest id from the run and sleep until the next tick.
The created[gte] / created[lte] window is applied as bounds on the event id rather than as a scan over timestamps, so a narrow window is exact and cheap — backfilling one day out of the retained thirty costs about what reading that day costs.
For push instead of polling, register a webhook endpoint. The two are the same data; webhooks add delivery, signing and retries, and cost you a public HTTPS endpoint.
Event catalog
Thirty-three types today. New ones are added over time, so a consumer that switches on type must tolerate an unrecognised value rather than failing — and a webhook endpoint subscribed with "*" receives types added after it was registered.
| Type | Fires when |
|---|---|
passport.created | A passport is created, individually or as an ontology's root. |
passport.updated | A passport's values, friendly_id, publish status or metadata change — one event per call. |
passport.deleted | A passport is deleted. |
template.created | A template is created or forked from another. |
template.updated | A draft template's structure changes, or a completed template's metadata does. |
template.deleted | A template is deleted. |
ontology.created | An ontology is instantiated from a completed template. |
ontology.updated | An ontology's metadata is patched — today the only direct edit an ontology accepts. |
ontology.version.published | A draft is published as a new schema version; the snapshot carries the version payload including its number. |
composition.created | A composition layout is created for an ontology. |
composition.updated | A composition layout is changed. Compositions have their own pair — a layout change does not surface as passport.updated. |
import.started | A mass import is accepted and begins processing. |
import.completed | An import finishes with every row applied. |
import.completed_with_errors | An import finishes with at least one rejected row; walk the import's rows for detail. |
import.failed | An import fails outright, applying nothing. |
import.rows.retried | Previously failed rows of an import are resubmitted. |
lab.transform.completed | A Lab transform run finishes successfully. |
lab.transform.failed | A Lab transform run fails. |
lab.rollback.completed | A Lab rollback finishes, reverting a previous run. |
lab.enabled | Synex Lab is enabled for the team; payload is {object, team, enabled, enabled_at}. |
lab.disabled | Synex Lab is disabled for the team; same purpose-built payload. |
webhook_endpoint.disabled | An endpoint is auto-disabled after failing continuously for five days. Delivered to your other healthy endpoints, never to the one being disabled. |
audit_settings.updated | A team's audit retention window changes. The change is not retroactive, so this announces the window future entries will be written with. |
api_key.created | An API key is minted through POST /v1/api_keys. The snapshot never carries the key's secret. |
api_key.rolled | A key is rolled, by either roll endpoint. Emitted on the replacement, whose rolled_from names the retiring key — so one event gives you both ids. No secret, again. |
api_key.revoked | A key is revoked. The snapshot is its final state; the row itself is already gone, so this is one of the few payloads that cannot be re-fetched. There is deliberately no api_key.updated. |
design.updated | The team's public-passport theme changes — palette, rounding, shadow, logo or font. The snapshot is the whole design. |
file.created | A file becomes live in the Drive, from either ingestion path: a single-shot POST /v1/files or the completion of a multipart upload. Folders emit nothing. |
data_request.created | An external data-collection link is created. The snapshot never carries the request's token or secret. |
data_request.fulfilled | A third party submits the external form. Carries properties_filled — which properties were filled, never what they were filled with — and a null actor. |
supplier.linked | A supplier is linked to the team, whether by accepting an invitation or through POST /v1/suppliers/link. |
supplier_request.created | A team sends a linked supplier a request for named values. |
supplier_request.fulfilled | The supplier submits their answers in the supplier portal. Emitted for the requesting team, with a null actor. Mapping those answers onto a passport is a separate act and emits passport.updated. |
Remember the two catalog-wide rules when reading that list: no type fires on a read, and no type fires per-row inside a mass import.
Events with no actor
A third thing to carry away, which the newest types are the first to make true: some events have no acting user. data_request.fulfilled and supplier_request.fulfilled are caused by someone outside your team — a stranger holding a link, or a supplier working in their own portal — so actor.user_id and actor.api_key_id are both null, and request_id names a request no key of yours ever made.
A consumer that assumes an actor is present will break on exactly these two. Read the actor as optional everywhere and the question never comes up again.