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 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
AnswersWhat changedWho did what, reads included
PurposeDriving integrationsCompliance and forensics
Per-entry idYes (evt_…), stable and deduplicableNo — entries are addressed by log position
Reads recordedNeverYes, every passport read
RetentionFixed at 30 days for every teamConfigured per team by admins
Delivered by webhookYesNo
PayloadFull object snapshotThe 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 GET will ever appear here.
  • Mass imports emit batch-level events only. A 50,000-row import produces import.started and one terminal event — never 50,000 passport.created events. If you need per-row detail, walk GET /v1/imports/{import}/rows.
  • Idempotency replays never re-emit. A replayed POST returns 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.


GET/v1/events

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 type as 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

GET
/v1/events
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"
}

GET/v1/events/:id

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

GET
/v1/events/evt_2c7f9a4e1b8d3506
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:

  1. GET /v1/events?limit=100 with your filters.
  2. Process rows until you hit your stored high-water mark id.
  3. If you did not reach it and has_more is true, follow next_cursor.
  4. 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.

TypeFires when
passport.createdA passport is created, individually or as an ontology's root.
passport.updatedA passport's values, friendly_id, publish status or metadata change — one event per call.
passport.deletedA passport is deleted.
template.createdA template is created or forked from another.
template.updatedA draft template's structure changes, or a completed template's metadata does.
template.deletedA template is deleted.
ontology.createdAn ontology is instantiated from a completed template.
ontology.updatedAn ontology's metadata is patched — today the only direct edit an ontology accepts.
ontology.version.publishedA draft is published as a new schema version; the snapshot carries the version payload including its number.
composition.createdA composition layout is created for an ontology.
composition.updatedA composition layout is changed. Compositions have their own pair — a layout change does not surface as passport.updated.
import.startedA mass import is accepted and begins processing.
import.completedAn import finishes with every row applied.
import.completed_with_errorsAn import finishes with at least one rejected row; walk the import's rows for detail.
import.failedAn import fails outright, applying nothing.
import.rows.retriedPreviously failed rows of an import are resubmitted.
lab.transform.completedA Lab transform run finishes successfully.
lab.transform.failedA Lab transform run fails.
lab.rollback.completedA Lab rollback finishes, reverting a previous run.
lab.enabledSynex Lab is enabled for the team; payload is {object, team, enabled, enabled_at}.
lab.disabledSynex Lab is disabled for the team; same purpose-built payload.
webhook_endpoint.disabledAn endpoint is auto-disabled after failing continuously for five days. Delivered to your other healthy endpoints, never to the one being disabled.
audit_settings.updatedA team's audit retention window changes. The change is not retroactive, so this announces the window future entries will be written with.
api_key.createdAn API key is minted through POST /v1/api_keys. The snapshot never carries the key's secret.
api_key.rolledA 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.revokedA 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.updatedThe team's public-passport theme changes — palette, rounding, shadow, logo or font. The snapshot is the whole design.
file.createdA 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.createdAn external data-collection link is created. The snapshot never carries the request's token or secret.
data_request.fulfilledA third party submits the external form. Carries properties_filled — which properties were filled, never what they were filled with — and a null actor.
supplier.linkedA supplier is linked to the team, whether by accepting an invitation or through POST /v1/suppliers/link.
supplier_request.createdA team sends a linked supplier a request for named values.
supplier_request.fulfilledThe 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.

Was this page helpful?