Events
Inspect retained events and consume configured webhook deliveries.
Each account has an ordered, immutable event log. Gateway fans references from that log into the buffers of interested users and OIDC applications. Delivery always belongs to an explicitly configured consumer, so ordering, retries, and acknowledgement have one durable identity.
JSON-RPC uses Events/list and Events/get; configured consumers receive
webhook pushes.
Event envelope
Every event has a stable event_… id, an account-local seq, account_id,
created, type, provenance, and exactly one typed payload. See the
event type catalog for payloads.
- Order by
(account_id, seq); sequence numbers are monotonic but may have gaps. - Delivery is at least once. Deduplicate by
event.id. - Every configured consumer receives every variant authorized for its owner buffer. Consumer configuration and cursor identity contain no event selection.
- Dispatch the variants you need. After signature and envelope validation, safely acknowledge supported but irrelevant variants and unknown future variants instead of treating absence as delivery evidence.
Values added later
The published enum-field registry marks every enum-valued
field open or strict; it is the authority, so read it rather than a list
copied here. New values may appear in an open field without notice, so a
consumer must accept any well-formed value there; the SDK verifiers do.
failure.code is an open string: a code registered later is unknown to an
older consumer, and the Gateway never sends a code it has retired.
- Present an unknown open value generically. It is advisory only: it grants no authority, selects no action and is not retryable.
- An unknown Sent filing
statusnames a state whose other fields are unknown; an unknown submissionstatuscarries no status-specific facts. - A value in a
strictfield, an unknown field, and a malformed or retired value remain contract violations; reject such a delivery.
failure.message and failure.action are advisory text that the Gateway
renders from its current error catalog for every delivery attempt; only the
failure's facts (code, id, and the state it belongs to) are stored. A
redelivery of the same event.id may therefore carry a different message or
action than an earlier attempt. Deduplicate by event.id and never compare
advisory text across attempts.
List retained events
Events/list reads a page of retained history for one account. It does not
advance a delivery consumer.
{
"jsonrpc": "2.0",
"id": 1,
"method": "Events/list",
"params": {
"account_id": "account_01H9X...",
"types": ["email.*"],
"window": {
"after": "2026-07-01T00:00:00Z",
"before": "2026-07-13T00:00:00Z"
},
"page": { "size": 100 }
}
}Continue with page.next_token:
{
"account_id": "account_01H9X...",
"page": { "size": 100, "token": "eyJ..." }
}Get one event
Events/get resolves an event by its stable id within one account.
{
"jsonrpc": "2.0",
"id": 1,
"method": "Events/get",
"params": { "account_id": "account_01H9X...", "event_id": "event_3Nq7..." }
}Configure a consumer
EventConsumerConfig owns one mandatory push_endpoint. Every consumer uses
the same webhook push delivery contract:
| Wire type | Direction | Authentication and acknowledgement |
|---|---|---|
push_endpoint | Gateway → HTTP endpoint | Ed25519-signed envelope; HTTP success completes one event delivery |
There is no per-consumer signing secret. EventConsumerConfig/create returns the
consumer (consumer.id) and verification_jwks_url, the Gateway's public
webhook key set at GET /v1/webhooks/jwks. Keys rotate on the Gateway (a KMS key
rotation); the key set publishes only versions that are not yet retired.
Transport encryption is recommended for internet endpoints but is not required
by the API, which permits trusted internal Docker-network endpoints.
Signed push envelope
Webhook push carries exactly one event in a JSON-RPC notification batch. The signed HTTP body is an array even though each delivery contains one notification:
[
{
"jsonrpc": "2.0",
"method": "sync.completed",
"params": {
"id": "event_3Nq7...",
"seq": "42",
"type": "sync.completed",
"account_id": "account_01H9X...",
"created": "2026-07-21T00:00:00Z",
"idempotency_key": "...",
"sync_completed": {
"count": "12",
"job_id": "syncjob_...",
"operation_id": "operation_..."
}
}
}
]Every delivery carries these headers:
| Header | Value |
|---|---|
traummail-event-id | The event id; must equal params.id |
traummail-timestamp | Unix seconds the signature covers |
traummail-consumer-id | The consumer id the delivery is bound to (informational) |
traummail-signature | v1=<kid>:<base64url 64-byte Ed25519 signature>, kid = webhook-v<N> |
The Gateway KMS key gateway-webhook-signer signs exactly
"traummail/webhook/v1\0" || consumer_id || "\0" || timestamp || "." || bodywhere consumer_id is your consumer's EventConsumer.id and body is the exact
raw array bytes. Verify with your own configured consumer id (never the
header value) against the published key set before parsing: reject timestamps
more than 300 seconds from your clock in either direction, unknown kids, and
failing signatures. Because the consumer id is signed, a delivery for one
consumer cannot be replayed to another. Then require the strict one-notification shape, validate
params as an event, require method to equal params.type, and require the
traummail-event-id header to equal params.id. Delivery is at-least-once:
deduplicate on event.id; a successful response acknowledges only that event.
The SDKs implement exactly this check: Rust
verify_signed_event(event_id, consumer_id, timestamp, signature, body, jwks, options)
and TypeScript
verifyWebhookRequest(request, { verificationJwks, consumerId }, options) or
new WebhookPushConsumer({ verificationJwks, consumerId }, options).
Retention and recovery
The retained event log is an inspection and projection source, not permanent
storage. There is no public Tail, Subscribe, ReplayEvents, or
ReingestEvents operation. To import provider history, start an explicit
AccountSynchronizations history synchronization. Newly observed changes then
enter the normal event log and consumer fan-out path.