Supermail
API reference

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 status names a state whose other fields are unknown; an unknown submission status carries no status-specific facts.
  • A value in a strict field, 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.

JSON-RPC request
{
  "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 typeDirectionAuthentication and acknowledgement
push_endpointGateway → HTTP endpointEd25519-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:

HeaderValue
traummail-event-idThe event id; must equal params.id
traummail-timestampUnix seconds the signature covers
traummail-consumer-idThe consumer id the delivery is bound to (informational)
traummail-signaturev1=<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 || "." || body

where 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.

On this page