> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dairo.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Read the durable event ledger

> Read the durable event ledger (catch-up)



## OpenAPI

````yaml GET /v1/events
openapi: 3.1.0
info:
  title: Dairo API
  version: 1.0.0
  summary: Email infrastructure API for AI agents and applications.
  description: >-
    Source-of-truth public Dairo API contract. Operations marked
    x-dairo-status=live are implemented; planned operations define the SDK/CLI
    target surface.
servers:
  - url: https://api.dairo.app
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Messages
    description: Outbound email sending.
  - name: Domains
    description: >-
      A domain lets you send and receive on addresses you own
      (`hello@yourcompany.com`) instead of the shared `dairo.app` domain. Add a
      domain, publish the DNS records Dairo returns, then recheck it to move
      from `pending` to `verified`. Every account also has read-only access to
      the included `dairo.app` domain, which is always verified and cannot be
      modified.
  - name: Inboxes
    description: >-
      An inbox is a send-and-receive address on one of your domains
      (`billing@yourcompany.com`) that carries a routing `mode`, an assigned
      `agent` name, and a `status` of `active`, `paused`, or `deleted`. Create
      an inbox on a `verified` domain, then mail sent to it is stored as a
      message and delivered to your webhooks as a `message.received` event. You
      can also mint an inbox on the shared `dairo.app` domain to receive without
      adding a domain of your own.
  - name: Webhooks
    description: >-
      Register HTTPS endpoints that receive Dairo account events — new inbound
      mail, delivery and bounce receipts, budget trips, letter and domain status
      changes, and more — as signed JSON `POST` requests. Each subscription has
      its own signing secret, and deliveries are retried automatically, logged
      per attempt, and can be paused, resumed, redriven, or test-pinged. Verify
      every request with the `X-Dairo-Signature` header before trusting its
      payload.
  - name: API Keys
    description: >-
      Programmatic credentials for the Dairo API. Each key holds an explicit set
      of `scopes` and an optional IP allowlist, and its secret is returned only
      once — at creation. A key that carries `keys:write` can mint additional
      keys, but only with scopes that are a subset of its own.
  - name: Messages
    description: >-
      Send messages and read what comes back, across every channel Dairo
      carries. One `POST /v1/messages` call delivers email, agent-to-agent,
      Telegram, Slack, or SMS from one of your inboxes, and `GET /v1/messages`
      returns inbound, outbound, and internal messages as a single unified list.
      The same surface exposes a send's delivery events, scheduled-send
      cancellation, bulk deletion, Telegram message actions, and attachment
      metadata.
  - name: Threads
    description: >-
      Threads group the inbound and outbound messages of a single email or
      agent-to-agent conversation under one `subject`, scoped to the inbox they
      belong to. Dairo opens and updates threads automatically as messages
      arrive and send, so this resource is read-only: list threads by recent
      activity, or fetch one thread with its messages. Both calls require the
      `messages:read` scope.
  - name: Audiences
    description: >-
      Reusable email recipient lists and list broadcasts. Create an audience,
      add members by raw address or by contact reference, then broadcast a
      single message that fans out asynchronously to every active member.
  - name: Outbound
    description: Outbound email history and delivery events.
  - name: Templates
    description: >-
      Reusable email templates written as React email components, backed by an
      append-only history of immutable versions. Creating a template publishes
      version `1`; publish a new version whenever the content changes, and
      re-point the mutable `currentVersion` pointer to roll forward or back
      instantly. Reference a template by `id` or `slug` when you [send a
      message](/sending/sending-email), and Dairo validates your `variables`
      against the pinned version's schema before the message goes out.
  - name: Budgets
    description: >-
      Budgets are fail-closed spending and volume ceilings enforced inside the
      send path. Attach one to your whole `account`, to a single API `key`, or
      to an `agent`, and every send is checked against its limits before it is
      accepted. A send over the line is rejected with a `budget_exceeded` error
      and emits a `budget.tripped` event.
  - name: Events
    description: >-
      A durable, ordered ledger of every delivery event on your account —
      outbound sends, delivery outcomes such as `email.delivered` and
      `email.bounced`, and inbound `message.received`. Read it forward from a
      cursor to catch up after downtime, spot lost events by their sequence
      gaps, and replay any slice back to your webhooks. Each ledger row mirrors
      the webhook event for the same source event, so the same `eventId` lines
      up across live delivery and replay.
  - name: Agents
    description: >-
      An Agent Passport gives one of your agents a durable, portable identity: a
      stable `agt_...` id and an account-wide Ed25519 signing key. Bind an API
      key or an inbox to a passport and every message that key or inbox sends
      carries a signed `X-Dairo-Provenance` header, letting any recipient
      confirm the message really came from your agent by checking it against
      Dairo's public JWKS. The verify and JWKS endpoints are public and
      unauthenticated; creating, reading, binding, and deleting passports run on
      your API key.
  - name: Compliance
    description: >-
      Prove and enforce EU data governance from your own automation. Export a
      tamper-evident, hash-chained audit ledger with an `EdDSA`-signed
      verification manifest, run irreversible subject-erasure and inbox-purge
      jobs that return signed deletion certificates, and read a
      machine-checkable residency posture with a monotonic subprocessor list —
      every signed artifact verifies against Dairo's published JWKS, so an
      auditor never has to trust the API on its word.
  - name: Conversation Autopilot
    description: >-
      Register a durable background wait on an inbox and let Dairo resolve an
      inbound email for you. Each wait watches the inbox's incoming mail; an
      EU-resident AI matcher decides which message satisfies a natural-language
      `instruction` and extracts the value you asked for — a one-time code, a
      reset link, or a confirmation line. The outcome arrives as a
      `verification.resolved` or `verification.expired` event on your webhook
      and the durable event ledger, or by polling the wait.
  - name: Letters
    description: >-
      Send physical mail by API. Dairo prints your PDF, envelopes it, and posts
      it, then keeps a live `status` and event timeline through delivery.
      Templates and batches personalize one design and send it to up to 1,000
      recipients per call.
  - name: Buckets
    description: >-
      Buckets are owner-scoped containers for stored files. Every account starts
      with a default bucket, plus a reserved bucket that holds inbound email
      attachments. Uploads follow a two-step flow — request an upload URL, `PUT`
      the bytes to it, then finalize to record the object — and every object is
      malware-scanned before it can be shared. Reads require the `buckets:read`
      scope; writes require `buckets:write`.
  - name: Contacts
    description: >-
      A channel-agnostic address book. Each contact is one identity — a
      `person`, an `agent`, or the project `self` contact — with any number of
      `handles`, one per channel address (`email`, `a2a`, `telegram`, `postal`,
      and more). Store a contact once and address it anywhere by `@alias`,
      `contact:<id>`, or `@me` instead of repeating raw addresses; Dairo also
      links inbound messages to the sender's contact so you can read a full
      cross-channel history.
  - name: Phone
    description: >-
      Place outbound AI phone calls and manage the phone numbers that place
      them. A call is asynchronous — you place it, then poll for its status,
      transcript, recording, and a structured summary — while numbers are
      searched in carrier inventory, provisioned, bound to an inbox or agent,
      and released. Reads use the `phone:read` scope, number provisioning uses
      `phone:write`, and dialing a person uses `phone:call`.
  - name: Slack
    description: >-
      Connect Slack workspaces so a bot running on your own Slack app appears in
      the unified inbox. Register a managed or bring-your-own Slack app, mint an
      "Add to Slack" install URL for your customers, and inbound @mentions and
      DMs arrive as threads that fire the `message.received` webhook. See
      [connecting Slack](/channels/slack) for the end-to-end flow.
  - name: Account
    description: >-
      Introspect the account behind the API key making the request — its
      identity, the key's scopes, the current plan, live usage counters, and
      remaining storage. The resource is a single read-only call you use to
      confirm a key works and to check headroom before a large send or upload.
  - name: Attachments
    description: >-
      Attachments are the files carried on a message — the documents and images
      on inbound email, and files you attach when sending. These endpoints read
      a stored attachment: fetch its raw bytes directly, or mint a short-lived
      signed URL you can hand to a browser or another service. Every read is
      scoped through the message that owns the attachment and gated by a
      security scan, and all three require the `messages:read` scope.
  - name: Audit Logs
    description: >-
      An append-only, tamper-evident record of governance-relevant control-plane
      actions on your account — resource creation and deletion, API key and
      webhook management, membership and organization changes. Entries are
      written automatically as you and your keys act; they cannot be created,
      edited, or deleted through the API. Reading requires the `account:read`
      scope.
  - name: MCP
    description: >-
      A single public endpoint that advertises the Dairo hosted MCP server's
      tool surface — which tools an agent can call, the scopes each one needs,
      and a content-hash version you can poll cheaply. Point an MCP-aware client
      at it to discover Dairo's tools without authenticating, or pass a key to
      see which tools that key is allowed to call.
  - name: Notifications
    description: >-
      Control which categories of account email Dairo sends you. Preferences are
      per category — `account`, `billing`, `usage`, `security`, and `product` —
      and each defaults to enabled, so a new account receives every notification
      until you opt a category out.
  - name: Organizations
    description: >-
      An organization is a Dairo project — the isolation tier that owns your
      domains, inboxes, API keys, and every other resource, and that billing and
      membership attach to. Every account belongs to at least one organization
      and holds a `role` of `owner`, `admin`, or `member` in each. These
      dashboard-authenticated endpoints back the organization switcher: they
      list the organizations you belong to and create new ones.
  - name: Reputation
    description: >-
      Every agent carries its own bounce and complaint reputation, tracked over
      a rolling 24-hour window and isolated from your domain and from your other
      agents. When an agent's rates climb, Dairo throttles it and then pauses it
      automatically to protect your sending reputation, exposing the current
      state, the thresholds in force, and a `shouldSend` verdict you can check
      before a batch. Use these endpoints to watch the fleet and to clear an
      agent once you've fixed the underlying cause.
  - name: Schema Inboxes
    description: >-
      Attach an extraction contract to an inbox so inbound mail arrives as
      typed, validated JSON instead of best-effort text. The contract is a flat
      `JSON-Schema-lite` map of field names to type declarations; each incoming
      message is extracted against it, and any extracted object that fails
      validation is either quarantined or passed through per the inbox's policy.
      These endpoints are the control plane — attach, read, and detach the
      contract — while extraction runs automatically on every message the inbox
      receives.
  - name: Telegram
    description: >-
      The catalog of text-to-speech voices you can attach to a
      [Telegram](/channels/telegram) voice message. Browse it to find a voice,
      then pass that voice's `slug` as `telegram.voice.voiceId` when you send.
      Omitting `voiceId` uses the account default voice (`adrian`).
paths:
  /v1/events:
    get:
      tags:
        - Events
      summary: Read the durable event ledger (catch-up)
      description: >-
        Returns your account's durable, ordered event ledger — outbound message
        lifecycle (`email.sent`, `email.delivered`), delivery outcomes
        (`email.bounced`, `email.complained`), and inbound `message.received` —
        as a `data` array of ledger rows, each carrying a monotonic
        per-partition `seq` and a `partitionKey` join key. Events are returned
        oldest-first (forward catch-up order), so you replay history in the
        order it happened; page forward by passing the previous response's
        `pagination.nextCursor` as `since`. `limit` accepts 1–100 and defaults
        to 50. Narrow the stream with `inboxId` (one partition), `type` (one
        event type, e.g. `email.bounced`), or `idempotencyKey` (every event
        threaded from one idempotent send). Each page also reports `gaps[]` —
        per-partition `seq` numbers expected but absent within the returned
        window (detection is window-local, not whole-history); a non-empty entry
        means events were lost and can be recovered with a replay. This call is
        read-only and never re-delivers to webhooks. Requires the `events:read`
        scope.
      operationId: listEvents
      parameters:
        - name: since
          in: query
          required: false
          description: >-
            Opaque keyset cursor (from a prior pagination.nextCursor) to read
            forward from.
          schema:
            type: string
        - $ref: '#/components/parameters/Limit'
        - name: inboxId
          in: query
          required: false
          description: Filter to one inbox partition.
          schema:
            type: string
        - name: type
          in: query
          required: false
          description: Filter to one event type, e.g. email.bounced.
          schema:
            type: string
        - name: idempotencyKey
          in: query
          required: false
          description: Return every event carrying this loop-wide idempotency key.
          schema:
            type: string
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventLedgerPage'
        '400':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
  schemas:
    EventLedgerPage:
      type: object
      required:
        - object
        - data
        - pagination
        - gaps
      additionalProperties: false
      properties:
        object:
          type: string
          enum:
            - list
          description: Always "list" — the unified list envelope discriminator.
        data:
          type: array
          items:
            $ref: '#/components/schemas/LedgerEvent'
          description: >-
            The ledger events for this page, each with seq/partitionKey join
            keys. (This is the array named `data`, not `events`.)
        pagination:
          $ref: '#/components/schemas/Pagination'
        gaps:
          type: array
          items:
            $ref: '#/components/schemas/LedgerGap'
          description: >-
            Per-partition seq gaps detected within this page's window. An empty
            array means no gap was detected in the returned window — detection
            is window-local (only the seq range present on this page), not
            whole-history.
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/Error'
      additionalProperties: false
    LedgerEvent:
      type: object
      description: >-
        One durable, ordered row of the reliability-spine event ledger. Mirrors
        the webhook event for the same source event (same eventId/type/data) and
        adds the monotonic per-partition seq plus the loop-wide idempotencyKey.
      required:
        - eventId
        - type
        - seq
        - partitionKey
        - createdAt
      additionalProperties: false
      properties:
        eventId:
          type: string
          description: >-
            The evt_<uuid> id; identical to the X-Dairo-Event-Id of the original
            webhook delivery, so consumer-side dedupe is exact across live
            delivery and replay.
        type:
          type: string
          description: >-
            Event type, e.g. email.sent, email.delivered, email.bounced,
            email.complained, message.received.
        seq:
          type: integer
          format: int64
          description: >-
            Monotonic, 1-based, contiguous sequence number within the partition.
            A gap in seq is a lost event (surfaced in gaps[]).
        partitionKey:
          type: string
          description: The ordering domain for seq, e.g. inbox:<uuid>.
        inboxId:
          type:
            - string
            - 'null'
          format: uuid
        threadId:
          type:
            - string
            - 'null'
          format: uuid
        idempotencyKey:
          type:
            - string
            - 'null'
          description: >-
            Loop-wide idempotency key threaded from the originating send onto
            every status event and the matched inbound reply. Null when the
            event did not originate from an idempotent send.
        outboundMessageId:
          type:
            - string
            - 'null'
          format: uuid
        messageId:
          type:
            - string
            - 'null'
          format: uuid
        providerMessageId:
          type:
            - string
            - 'null'
        occurredAt:
          type:
            - string
            - 'null'
          format: date-time
        createdAt:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: true
          description: The original webhook event data payload (metadata-first, body-free).
    Pagination:
      type: object
      required:
        - nextCursor
      properties:
        nextCursor:
          type:
            - string
            - 'null'
      additionalProperties: false
    LedgerGap:
      type: object
      required:
        - partitionKey
        - missingSeq
      additionalProperties: false
      properties:
        partitionKey:
          type: string
          description: >-
            The partition (ordering domain) the gap was detected in, e.g.
            inbox:<uuid>.
        missingSeq:
          type: array
          items:
            type: integer
            format: int64
          description: >-
            Sequence numbers expected-but-absent within the returned window for
            this partition. A non-empty list means one or more events were lost;
            replay to recover.
    Error:
      type: object
      required:
        - message
      properties:
        message:
          type: string
        code:
          type: string
        type:
          type:
            - string
            - 'null'
        param:
          type:
            - string
            - 'null'
      additionalProperties: false
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Dairo API key, e.g. dairo_test_... or dairo_live_...

````