Logging events with the Events API

Copy page

Log application-level events for the agents framework using the existing run-API auth.

Overview

The Events API is a write-only ingest endpoint for application-level events emitted from any context that already authenticates against the run API: browser widgets, server-side agent code, or external SDK consumers. Events are persisted to the run-database, scoped per project, and serve as the source of truth for downstream analytics and outbound webhook delivery.

Endpoint

  • Path: /run/v1/events
  • Method: POST
  • Auth: Existing run-API strategies (API key or OAuth bearer token via the support_copilot app credential path).
  • Content-Type: application/json

Authentication

Choose the authentication method:

See Authentication → Run API for more details.

Request body

{
  "id": "optional-client-supplied-id",
  "type": "user_message_submitted",
  "agentId": "support-agent",
  "conversationId": "conv-abc",
  "messageId": "msg-xyz",
  "properties": { "source": "chat-widget" },
  "userProperties": { "userId": "u_123", "plan": "pro" },
  "metadata": { "appVersion": "1.4.2" }
}
FieldRequiredNotes
typeyesFree-form string. Use the well-known lifecycle event names (below) for widget-emitted events; consumers may use any other names.
idnoClient-supplied ID enables idempotency (see Idempotency below). Server generates one if omitted.
agentIdnoServer fills from the authenticated execution context if absent.
conversationId, messageIdnoAnchor the event to a specific conversation or message. Both are independently optional — events can be free-form (no anchor).
propertiesnoFree-form JSON object describing the event.
userPropertiesnoFree-form JSON object identifying the end-user. See User attribution for recommended field conventions.
metadatanoFree-form JSON object — the caller's namespace. The server never reads or writes to this column. Any keys you put here (including a key called authMethod) are stored as-is. Server-controlled debug context lives in a separate serverMetadata column on responses (see below).

Responses

StatusMeaning
201 CreatedA new event row was inserted.
200 OKAn event with the supplied id already existed for (tenantId, projectId). The persisted row is returned unchanged (idempotent insert).
400 Bad RequestThe request body failed validation (most commonly: missing type).
401 UnauthorizedAuthentication failed.

Both 200 and 201 responses share the shape { "data": { /* persisted event */ } }. The persisted row includes a server-controlled serverMetadata object alongside the caller-supplied metadata. serverMetadata.authMethod reflects the authentication strategy that produced the request; this field cannot be set or overridden by the caller (the request schema does not accept serverMetadata).

Idempotency

Pass a stable id value on retries. If a row with that (tenantId, projectId, id) triple already exists, the server returns 200 with the original row — the original createdAt is preserved and the event is not modified. This makes browser-side fire-and-forget calls safe to retry.

User attribution

userProperties is a free-form JSON object — the framework accepts arbitrary keys with arbitrary values and stores the object as-is. The fields below are recommended conventions for cross-product consistency, not server-validated shapes:

FieldRecommended typeNotes
idstring | numberCaller-supplied widget identifier.
userIdstring | numberThe end-user identifier.
supportAgentNamestringIf a support agent is acting on behalf of the user.
userTypestringCaller-defined user type.
(extras)anyAny other keys are stored as-is.

The server does not validate field types within userProperties{ userId: 123 } and { userId: "u_abc" } are both accepted. Customers should pick a convention and stick to it within their own data.

userProperties is caller-trusted analytics attribution metadata. Caller-supplied values always win on every authentication path — the server never overrides them. When userProperties is absent from the request body, the server resolves it from the message anchor (when messageId is supplied), then the conversation anchor (when conversationId is supplied or derivable from the message), and finally falls back to null. The server does not merge individual fields — userProperties is treated as an opaque object — and does not auto-fill from the authenticated execution context. For server-verified user identity, read conversation.userId (the cryptographically verified sub claim from JWT auth, populated on every chat turn).

The chat-handler write path (the indirect source of conversation/message anchors) drops widget-synthesized auto-mint identities (identificationType of 'ANONYMOUS' or 'COOKIED') and strips the identificationType marker from anything else. This keeps placeholder anonymous identities from polluting webhook payloads. Direct POST /run/v1/events callers receive the same defense — auto-mint shapes in the request body are filtered the same way before persistence.

For server-authoritative auth audit (which authentication strategy was used), see serverMetadata.authMethod — that field is server-controlled and immutable to callers.

PII content placed in userProperties is the caller's responsibility — the framework does not redact or allowlist field contents.

Free-form events

The schema accepts events with no anchor (conversationId, messageId, and agentId are all independently optional). Receivers downstream of the events table define their own policy for non-anchored events.

Event vocabulary

The framework does not ship an event-name registry. The type field is a free-form string and consumers define their own vocabulary. Two precedents to follow if you're authoring a consumer:

  • Inkeep widget chat events are typed by @inkeep/agents-ui's InkeepEvent discriminated union (chat, search, and widget event categories). When the widget self-instruments lifecycle events in a future release, those names are the canonical widget vocabulary.
  • Consumer-specific event names (action events, generation events, custom product events) live in the consumer's own package as a const + derived union pattern. They flow through the API as plain strings.

Webhook destinations subscribe to event families (conversation events, feedback events, event.created), not to specific type values. Receivers filter on type after delivery — this keeps the framework decoupled from any particular event vocabulary.

Outbound webhook delivery

After a successful insert, the route dispatches the persisted event to every webhook destination subscribed to the event.created family for the project (and matching the resolved agent scope). Delivery is fire-and-forget — the existing webhook delivery infrastructure handles retries, signing, and SSRF protection. Subscribe a destination by checking the Event Created option on the webhook destination form.

Webhook dispatch requires both an agent scope (agentId either supplied in the request body or inferred from the API key's authenticated context) and a resolved ref on the request. Free-form events without an agentId — or events authenticated without a resolvable agent ref — are still persisted to the events table, but no webhook fires for them. Operators can confirm via the 'Skipping event.created webhook dispatch' debug log on the run-events logger when this code path is taken.

On this page