openapi: 3.0.3
info:
  title: MailBench SMTP API
  version: 1.0.0
  description: |
    Capture the email your app sends and test against it. Point your app's SMTP
    settings at an inbox, then use this API to read what arrived, wait for a
    specific email in CI, receive webhooks, and clean up.

    **Nothing is ever delivered onward.** The service has no outbound mail code,
    so it cannot be used as a relay.

    ## Authentication

    Send an API key as a bearer token:

    ```
    Authorization: Bearer sbx_api_...
    ```

    A key belongs to one workspace and is either `read` or `read_write`. Create keys
    in the dashboard (they are shown once). Keys can read team data but cannot
    administer it: inviting people, managing keys and billing need a signed-in
    dashboard session and are not part of this API.

    ## Responses

    Every response is a JSON envelope. Success:

    ```json
    { "success": true, "data": { } }
    ```

    Failure:

    ```json
    { "success": false, "error": { "code": "not_found", "message": "Inbox not found" } }
    ```

    Another workspace's objects answer `404`, never `403`, so IDs cannot be probed.
    List endpoints add a top-level `next_cursor` when there is more.

    ## Limits

    Your plan sets monthly emails, retention, webhooks and requests per second.
    Going over the request rate answers `429` with `Retry-After`. Creating more
    than your plan allows answers `402` with code `limit_reached`.

    ## Not covered here

    Sign-in, workspace and team administration, API-key management and billing are
    dashboard-only. SMTP relay (releasing a message to a real mailbox) exists but is
    not yet documented.
  contact:
    name: MailBench SMTP support
servers:
  - url: https://app.mailbench.dev
    description: Replace with your deployment's API origin (the same origin that serves the dashboard).
security:
  - bearerAuth: []
tags:
  - name: Inboxes
    description: An inbox has its own SMTP username and password.
  - name: Messages
    description: Captured emails.
  - name: Waiting
    description: Block until an email arrives. Built for CI.
  - name: Tags
    description: Label messages by hand or with rules.
  - name: Webhooks
    description: Signed HTTP callbacks for new messages.
  - name: Checks
    description: HTML, link and spam analysis of a message.
  - name: Sending
    description: Put a message into an inbox over HTTP, without SMTP.
  - name: Testing
    description: Rendered views and failure simulation.
  - name: Workspace
    description: The workspace your key belongs to.

paths:
  /v1/workspace:
    get:
      tags: [Workspace]
      operationId: getWorkspace
      summary: Get the current workspace
      responses:
        '200':
          description: The workspace.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Workspace' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/usage:
    get:
      tags: [Workspace]
      operationId: getUsage
      summary: Emails used this month against your plan
      description: The quota period is the calendar month in UTC.
      responses:
        '200':
          description: Usage and limits. `null` limits mean unlimited.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Usage' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes:
    get:
      tags: [Inboxes]
      operationId: listInboxes
      summary: List inboxes
      responses:
        '200':
          description: Inboxes with message counts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Inbox' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Inboxes]
      operationId: createInbox
      summary: Create an inbox
      description: |
        Returns the SMTP credentials. **The password is shown once**; rotate it with
        `POST /v1/inboxes/{id}/credentials` if you lose it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 100, example: Staging app }
      responses:
        '201':
          description: The inbox and its credentials.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/InboxWithCredentials' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/LimitReached' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: An inbox with that name already exists.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    get:
      tags: [Inboxes]
      operationId: getInbox
      summary: Get an inbox
      responses:
        '200':
          description: The inbox.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Inbox' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Inboxes]
      operationId: deleteInbox
      summary: Delete an inbox and every message in it
      responses:
        '200': { $ref: '#/components/responses/DeletedMessages' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/credentials:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    post:
      tags: [Inboxes]
      operationId: rotateInboxCredentials
      summary: Rotate the SMTP password
      description: The old password stops working immediately. The new one is shown once.
      responses:
        '200':
          description: The inbox with its new credentials.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/InboxWithCredentials' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/messages:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    get:
      tags: [Messages]
      operationId: listMessages
      summary: List messages in an inbox
      description: |
        Newest first, as summaries (no body, headers or attachment list; fetch one
        message for those). Filters combine with AND.
      parameters:
        - $ref: '#/components/parameters/Q'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/Subject'
        - $ref: '#/components/parameters/Since'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of message summaries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/MessageSummary' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Inboxes]
      operationId: clearInbox
      summary: Delete every message in an inbox
      description: The inbox and its credentials stay.
      responses:
        '200': { $ref: '#/components/responses/DeletedMessages' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/messages/wait:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    get:
      tags: [Waiting]
      operationId: waitForMessage
      summary: Wait for a message
      description: |
        Returns the newest message matching the filters **in full**, immediately if one
        already exists, otherwise as soon as one arrives. Subscribes before it first
        looks, so an email that lands in between is not missed.

        Use `since` to ignore mail from earlier test runs. If nothing arrives in time
        the answer is `404` with code `wait_timeout`.
      parameters:
        - $ref: '#/components/parameters/Q'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/Subject'
        - $ref: '#/components/parameters/Since'
        - name: timeout
          in: query
          description: Seconds to wait. Defaults to 20, capped at 30.
          schema: { type: number, exclusiveMinimum: true, minimum: 0, default: 20, maximum: 30 }
      responses:
        '200':
          description: The matching message.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/MessageDetail' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: No inbox, or no matching message within the timeout (`wait_timeout`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/events:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    get:
      tags: [Messages]
      operationId: streamInboxEvents
      summary: Stream new messages (server-sent events)
      description: |
        Keeps the connection open and sends one `message` event per new email, with the
        message summary as JSON in `data` and a cursor in `id`. Reconnect with
        `Last-Event-ID` to resume without gaps. The server ends a stream after about
        30 minutes; reconnect.
      parameters:
        - name: Last-Event-ID
          in: header
          schema: { type: string }
      responses:
        '200':
          description: An `text/event-stream`.
          content:
            text/event-stream:
              schema: { type: string }
              example: |
                id: 2026-10-12T10:00:00Z|msg_abc
                event: message
                data: {"id":"msg_abc","subject":"Welcome"}
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/send:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    post:
      tags: [Sending]
      operationId: sendToInbox
      summary: Put a message into an inbox over HTTP
      description: |
        Builds a MIME message and stores it as if it had arrived over SMTP. It counts
        toward your monthly quota. Nothing is delivered to the recipients.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SendRequest' }
      responses:
        '201':
          description: The stored message's ID.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [id]
                        properties:
                          id: { type: string, example: msg_c4p2f3a9wq }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402':
          description: |
            The workspace has used its monthly email allowance (`quota_reached`). Sending over SMTP answers `552` in
            the same situation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Messages]
      operationId: getMessage
      summary: Get a message in full
      description: |
        Includes text and HTML bodies, headers, extracted links, attachments and
        short-lived signed URLs (`files`) that a browser can open without an
        `Authorization` header.
      responses:
        '200':
          description: The message.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/MessageDetail' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Messages]
      operationId: deleteMessage
      summary: Delete a message
      responses:
        '200': { $ref: '#/components/responses/DeletedMessages' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/raw:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Messages]
      operationId: getMessageRaw
      summary: Download the raw message (.eml)
      responses:
        '200':
          description: The original RFC 822 source.
          content:
            message/rfc822:
              schema: { type: string, format: binary }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/html:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Messages]
      operationId: getMessageHTML
      summary: The HTML body
      description: Served under a restrictive `Content-Security-Policy` (no scripts, opaque origin). 404 if the message has no HTML part.
      responses:
        '200':
          description: The HTML part.
          content:
            text/html:
              schema: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/links:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Messages]
      operationId: getMessageLinks
      summary: Links found in the message
      description: The http(s) links, extracted from the HTML and text parts.
      responses:
        '200':
          description: The links.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { type: string, format: uri }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/attachments/{index}:
    parameters:
      - $ref: '#/components/parameters/MessageId'
      - $ref: '#/components/parameters/AttachmentIndex'
    get:
      tags: [Messages]
      operationId: getAttachment
      summary: Download an attachment
      responses:
        '200':
          description: The file, with its original content type.
          content:
            '*/*':
              schema: { type: string, format: binary }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/read:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    put:
      tags: [Messages]
      operationId: markMessageRead
      summary: Mark a message as read
      responses:
        '200': { $ref: '#/components/responses/ReadState' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Messages]
      operationId: markMessageUnread
      summary: Mark a message as unread
      responses:
        '200': { $ref: '#/components/responses/ReadState' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/batch:
    post:
      tags: [Messages]
      operationId: batchMessages
      summary: Act on many messages at once
      description: |
        Select messages either by `ids` or by a search `query` (optionally limited to
        one `inbox_id`), then apply an action. `tag` and `untag` need `tags`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BatchRequest' }
      responses:
        '200':
          description: How many messages were affected.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/BatchResult' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/tags:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    post:
      tags: [Tags]
      operationId: addMessageTag
      summary: Add a tag to a message
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                tag: { type: string, example: billing }
      responses:
        '200':
          description: The tag that was added.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [tag]
                        properties:
                          tag: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      tags: [Tags]
      operationId: setMessageTags
      summary: Replace a message's tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tags]
              properties:
                tags:
                  type: array
                  items: { type: string }
      responses:
        '200':
          description: The tags now on the message.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [tags]
                        properties:
                          tags:
                            type: array
                            items: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/tags/{name}:
    parameters:
      - $ref: '#/components/parameters/MessageId'
      - name: name
        in: path
        required: true
        description: The tag to remove. URL-encode it.
        schema: { type: string }
    delete:
      tags: [Tags]
      operationId: removeMessageTag
      summary: Remove a tag from a message
      responses:
        '200':
          description: The tag that was removed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [removed]
                        properties:
                          removed: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/tags:
    get:
      tags: [Tags]
      operationId: listTags
      summary: List tags in use
      responses:
        '200':
          description: Tags with their colors and message counts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Tag' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/tag-rules:
    get:
      tags: [Tags]
      operationId: listTagRules
      summary: List tag rules
      description: A rule tags matching messages as they arrive.
      parameters:
        - name: inbox_id
          in: query
          description: Only rules for this inbox.
          schema: { type: string }
      responses:
        '200':
          description: The rules.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/TagRule' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Tags]
      operationId: createTagRule
      summary: Create a tag rule
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TagRuleInput' }
      responses:
        '201':
          description: The rule.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TagRule' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: List webhooks
      description: Signing secrets are never listed.
      responses:
        '200':
          description: The webhooks.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Webhook' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Create a webhook
      description: |
        Each new message is POSTed to `url` as JSON, signed with the returned `secret`
        (shown once). Failed deliveries are retried. How many webhooks you can have
        depends on your plan (`402 limit_reached` beyond it).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, example: 'https://ci.example.com/hooks/email' }
                inbox_id:
                  type: string
                  nullable: true
                  description: Limit to one inbox. Omit for every inbox in the workspace.
      responses:
        '201':
          description: The webhook, including its signing secret.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/WebhookCreated' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/LimitReached' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/WebhookId'
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Delete a webhook
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [deleted]
                        properties:
                          deleted: { type: integer, example: 1 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/webhooks/{id}/deliveries:
    parameters:
      - $ref: '#/components/parameters/WebhookId'
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Recent deliveries
      description: The last 50, with status, attempts and the last HTTP status or error.
      responses:
        '200':
          description: The deliveries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Delivery' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/WebhookId'
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Send a test event
      description: Queues a `webhook.test` event. Check the result under deliveries.
      responses:
        '202':
          description: Queued.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [delivery_id]
                        properties:
                          delivery_id: { type: integer, format: int64 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/html-check:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Checks]
      operationId: checkHTML
      summary: Email-client support for the HTML and CSS
      description: Compares the HTML against the Can I email data set, per client.
      responses:
        '200':
          description: Findings and totals. Field names are capitalised.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/HTMLCheck' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/link-check:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Checks]
      operationId: checkLinks
      summary: Check that the links work
      description: Requests each link and reports its status. Private addresses are refused.
      parameters:
        - name: follow
          in: query
          description: Follow redirects and report the final target.
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: One result per link. Field names are capitalised.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/LinkCheck' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/spam-check:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      tags: [Checks]
      operationId: checkSpam
      summary: Spam score
      description: Scores the message with SpamAssassin rules when the server has one configured.
      responses:
        '200':
          description: The score and the rules that fired. Field names are capitalised.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SpamCheck' }
        '400':
          description: SpamAssassin is not configured on this server (`not_configured`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{inbox_id}/messages/latest/view.html:
    parameters:
      - $ref: '#/components/parameters/InboxIdSnake'
    get:
      tags: [Testing]
      operationId: viewLatestHTML
      summary: The latest message, rendered as HTML
      description: Handy for screenshot and visual-regression tests.
      responses:
        '200':
          description: The HTML.
          content:
            text/html:
              schema: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{inbox_id}/messages/latest/view.txt:
    parameters:
      - $ref: '#/components/parameters/InboxIdSnake'
    get:
      tags: [Testing]
      operationId: viewLatestText
      summary: The latest message, as plain text
      responses:
        '200':
          description: The text.
          content:
            text/plain:
              schema: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/chaos:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    get:
      tags: [Testing]
      operationId: getInboxChaos
      summary: Get the failure-simulation settings
      description: |
        Chaos makes the inbox's SMTP server fail on purpose, so you can test how your
        app handles refused senders, refused recipients or failed logins.
      responses:
        '200':
          description: The settings.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Chaos' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      tags: [Testing]
      operationId: setInboxChaos
      summary: Set the failure-simulation settings
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ChaosInput' }
      responses:
        '200':
          description: The settings now in force.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Chaos' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/messages/{id}/attachments/{index}/thumb:
    parameters:
      - $ref: '#/components/parameters/MessageId'
      - $ref: '#/components/parameters/AttachmentIndex'
    get:
      tags: [Messages]
      operationId: getAttachmentThumbnail
      summary: Thumbnail of an image attachment
      responses:
        '200':
          description: A small PNG or JPEG.
          content:
            image/*:
              schema: { type: string, format: binary }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/tags/{name}:
    parameters:
      - $ref: '#/components/parameters/TagName'
    put:
      tags: [Tags]
      operationId: updateTag
      summary: Rename a tag or change its color
      description: Renaming applies to every message that has the tag.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, description: The new name. Omit to keep it. }
                color: { type: string, description: 'A CSS color such as `#2563eb`. Empty for the automatic color.' }
      responses:
        '200':
          description: What changed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [old_name, name]
                        properties:
                          old_name: { type: string }
                          name: { type: string }
                          color: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Tags]
      operationId: deleteTag
      summary: Delete a tag
      description: Removes it from every message. The messages stay.
      responses:
        '200':
          description: The tag that was deleted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [deleted]
                        properties:
                          deleted: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/tag-rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
    put:
      tags: [Tags]
      operationId: updateTagRule
      summary: Replace a tag rule
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TagRuleInput' }
      responses:
        '200':
          description: The rule.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/TagRule' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Tags]
      operationId: deleteTagRule
      summary: Delete a tag rule
      description: Messages already tagged keep their tags.
      responses:
        '200':
          description: The rule that was deleted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [deleted]
                        properties:
                          deleted: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/webhook-deliveries/{id}/retry:
    parameters:
      - name: id
        in: path
        required: true
        description: Delivery ID, from the deliveries list.
        schema: { type: integer, format: int64 }
    post:
      tags: [Webhooks]
      operationId: retryWebhookDelivery
      summary: Retry a finished delivery
      description: Queues it again now with a fresh attempt budget. Only finished deliveries can be retried.
      responses:
        '200':
          description: The delivery that was queued again.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        required: [requeued]
                        properties:
                          requeued: { type: integer, format: int64 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{inbox_id}/messages/{message_id}/view.html:
    parameters:
      - $ref: '#/components/parameters/InboxIdSnake'
      - name: message_id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Testing]
      operationId: viewMessageHTML
      summary: A message, rendered as HTML
      responses:
        '200':
          description: The HTML.
          content:
            text/html:
              schema: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/inboxes/{id}/chaos/test:
    parameters:
      - $ref: '#/components/parameters/InboxId'
    post:
      tags: [Testing]
      operationId: testInboxChaos
      summary: Dry-run the failure simulation
      description: Rolls the dice once for each trigger and reports what would have happened. Nothing is sent.
      responses:
        '200':
          description: Whether each trigger fired, with the SMTP code and message.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          enabled: { type: boolean }
                          sender: { $ref: '#/components/schemas/ChaosOutcome' }
                          recipient: { $ref: '#/components/schemas/ChaosOutcome' }
                          authentication: { $ref: '#/components/schemas/ChaosOutcome' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An API key, `sbx_api_...`. `read` keys can only GET; `read_write` keys can change data.

  parameters:
    InboxId:
      name: id
      in: path
      required: true
      description: Inbox ID.
      schema: { type: string, example: box_g3bekcev4z7i4psq }
    InboxIdSnake:
      name: inbox_id
      in: path
      required: true
      description: Inbox ID.
      schema: { type: string, example: box_g3bekcev4z7i4psq }
    MessageId:
      name: id
      in: path
      required: true
      description: Message ID.
      schema: { type: string, example: msg_htumcicppd26jek6 }
    TagName:
      name: name
      in: path
      required: true
      description: The tag. URL-encode it.
      schema: { type: string }
    WebhookId:
      name: id
      in: path
      required: true
      schema: { type: string, example: wh_4mq7h2b9x1k3d8ra }
    AttachmentIndex:
      name: index
      in: path
      required: true
      description: Position in the message's `attachments` array, starting at 0.
      schema: { type: integer, minimum: 0 }
    Q:
      name: q
      in: query
      description: |
        Search. Free text matches subject, sender, recipients and body. Also supports
        `from:`, `to:`, `subject:`, `is:unread`, `is:read`, `is:tagged`, `tag:name`,
        `has:attachment`, `larger:1m`, `after:`/`before:`, negation with `-` or `!`,
        and `"exact phrases"`.
      schema: { type: string, example: 'is:unread has:attachment' }
    From:
      name: from
      in: query
      description: Sender contains this text.
      schema: { type: string }
    To:
      name: to
      in: query
      description: Any recipient contains this text.
      schema: { type: string }
    Subject:
      name: subject
      in: query
      description: Subject contains this text.
      schema: { type: string }
    Since:
      name: since
      in: query
      description: Only messages received at or after this time.
      schema: { type: string, format: date-time, example: '2026-10-12T10:00:00Z' }
    Limit:
      name: limit
      in: query
      description: Page size.
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    Cursor:
      name: cursor
      in: query
      description: The `next_cursor` from the previous page.
      schema: { type: string }

  responses:
    BadRequest:
      description: The request was malformed or a value was invalid.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    LimitReached:
      description: Your plan's limit for this would be exceeded (`limit_reached`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Forbidden:
      description: The key is read-only, or the action needs a dashboard session.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotFound:
      description: No such object in this workspace.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    RateLimited:
      description: Too many requests. Wait `Retry-After` seconds.
      headers:
        Retry-After:
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    DeletedMessages:
      description: How many messages were deleted.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data:
                    type: object
                    required: [deleted_messages]
                    properties:
                      deleted_messages: { type: integer }
    ReadState:
      description: The message's read state now.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  data:
                    type: object
                    required: [id, read]
                    properties:
                      id: { type: string }
                      read: { type: boolean }

  schemas:
    Envelope:
      type: object
      required: [success]
      properties:
        success: { type: boolean, example: true }
        data: {}
        next_cursor:
          type: string
          description: Present on list responses when there are more results.
    ErrorEnvelope:
      type: object
      required: [success, error]
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable and safe to switch on.
              example: not_found
            message:
              type: string
              description: For people. Wording can change.

    Workspace:
      type: object
      required: [id, name]
      properties:
        id: { type: string, example: ws_k3m9x2p7qa4d8fbn }
        name: { type: string, example: Acme }
        role:
          type: string
          enum: [owner, admin, member]
          description: Only set for dashboard sessions.

    Usage:
      type: object
      required: [period_start, used, monthly_limit, retention_days, members, member_limit, inboxes]
      properties:
        period_start: { type: string, format: date-time }
        used: { type: integer, description: Emails received this month., example: 2210 }
        monthly_limit: { type: integer, nullable: true, description: 'null means unlimited.', example: 2500 }
        retention_days: { type: integer, example: 7 }
        members: { type: integer, example: 3 }
        member_limit: { type: integer, nullable: true, example: 5 }
        inboxes: { type: integer, example: 2 }

    Inbox:
      type: object
      required: [id, name, smtp_username, message_count, created_at]
      properties:
        id: { type: string, example: box_g3bekcev4z7i4psq }
        name: { type: string, example: Staging app }
        smtp_username: { type: string, example: u_4f8k2m9x }
        message_count: { type: integer, example: 12 }
        created_at: { type: string, format: date-time }

    InboxWithCredentials:
      allOf:
        - $ref: '#/components/schemas/Inbox'
        - type: object
          required: [smtp]
          properties:
            smtp:
              type: object
              required: [host, ports, username, password]
              properties:
                host: { type: string, example: smtp.example.com }
                ports:
                  type: array
                  items: { type: integer }
                  example: [587, 465, 2525]
                username: { type: string }
                password: { type: string, description: Shown once. }
                note: { type: string }

    MessageSummary:
      type: object
      required:
        - id
        - inbox_id
        - received_at
        - envelope_from
        - recipients
        - from
        - to
        - subject
        - message_id
        - size
        - has_text
        - has_html
        - attachment_count
        - link_count
        - tls
        - read
        - tags
      properties:
        id: { type: string, example: msg_htumcicppd26jek6 }
        inbox_id: { type: string, example: box_g3bekcev4z7i4psq }
        received_at: { type: string, format: date-time }
        envelope_from: { type: string, description: The SMTP `MAIL FROM`., example: noreply@app.test }
        recipients:
          type: array
          items: { type: string }
          description: The SMTP `RCPT TO` addresses, including Bcc.
        from: { type: string, description: The `From` header., example: 'App <noreply@app.test>' }
        to: { type: string, description: The `To` header., example: 'Sam <sam@example.org>' }
        subject: { type: string, example: Welcome aboard }
        message_id: { type: string, description: The `Message-ID` header., example: '<8f2c1a@app.test>' }
        size: { type: integer, description: Size in bytes., example: 1432 }
        has_text: { type: boolean }
        has_html: { type: boolean }
        attachment_count: { type: integer, example: 0 }
        link_count: { type: integer, example: 2 }
        tls: { type: boolean, description: Whether the sender used TLS. }
        parse_error:
          type: string
          description: Set when the message is not valid MIME. Parts may be missing.
        read: { type: boolean, example: false }
        tags:
          type: array
          items: { type: string }

    Attachment:
      type: object
      required: [index, filename, content_type, size, url]
      properties:
        index: { type: integer, example: 0 }
        filename: { type: string, example: invoice.pdf }
        content_type: { type: string, example: application/pdf }
        size: { type: integer, example: 48211 }
        url:
          type: string
          description: Path to download it (needs your API key).
        thumb_url:
          type: string
          description: Path to a thumbnail, for images.

    MessageDetail:
      allOf:
        - $ref: '#/components/schemas/MessageSummary'
        - type: object
          required: [links, attachments, raw_url, files]
          properties:
            text: { type: string, example: 'Click https://app.test/verify/abc123 to confirm your address.' }
            html: { type: string, example: '<p>Click <a href="https://app.test/verify/abc123">here</a>.</p>' }
            links:
              type: array
              items: { type: string }
            headers:
              type: object
              additionalProperties:
                type: array
                items: { type: string }
            attachments:
              type: array
              items: { $ref: '#/components/schemas/Attachment' }
            raw_url: { type: string, description: Path to the raw `.eml`., example: /v1/messages/msg_htumcicppd26jek6/raw }
            html_url: { type: string, description: 'Path to the HTML body, when there is one.', example: /v1/messages/msg_htumcicppd26jek6/html }
            files:
              type: object
              description: Signed, expiring URLs a browser can open without an `Authorization` header.
              required: [raw, attachments, expires_at]
              properties:
                raw: { type: string, format: uri }
                html: { type: string, format: uri, example: '<p>Click <a href="https://app.test/verify/abc123">here</a>.</p>' }
                attachments:
                  type: array
                  items: { type: string, format: uri }
                thumbnails:
                  type: array
                  items: { type: string, format: uri }
                expires_at: { type: string, format: date-time }
            list_unsubscribe:
              type: object
              description: Parsed `List-Unsubscribe` headers, when present.
              properties:
                header: { type: string }
                header_post: { type: string }
                links:
                  type: array
                  items: { type: string }
                errors: { type: string }

    SendRequest:
      type: object
      required: [from, to, subject]
      properties:
        from: { $ref: '#/components/schemas/Address' }
        to:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/Address' }
        cc:
          type: array
          items: { $ref: '#/components/schemas/Address' }
        bcc:
          type: array
          items: { $ref: '#/components/schemas/Address' }
        reply_to:
          type: array
          items: { $ref: '#/components/schemas/Address' }
        subject: { type: string, example: Welcome aboard }
        text: { type: string, example: 'Hello from the API.' }
        html: { type: string, example: '<p>Hello from the API.</p>' }
        headers:
          type: object
          additionalProperties: { type: string }
        tags:
          type: array
          items: { type: string }
        attachments:
          type: array
          items:
            type: object
            required: [filename]
            properties:
              filename: { type: string }
              content_type: { type: string, default: application/octet-stream }
              content: { type: string, format: byte, description: Base64. }
    Address:
      type: object
      required: [email]
      properties:
        name: { type: string, example: Sam }
        email: { type: string, format: email, example: sam@example.org }

    BatchRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [read, unread, delete, tag, untag]
        ids:
          type: array
          items: { type: string }
          description: Messages to act on. Alternative to `query`.
        query:
          type: string
          description: A search expression (see `q`). Alternative to `ids`.
        inbox_id:
          type: string
          description: Limit a `query` to one inbox.
        tags:
          type: array
          items: { type: string }
          description: Required for `tag` and `untag`.
    BatchResult:
      type: object
      required: [action, affected]
      properties:
        action: { type: string, example: read }
        affected: { type: integer, format: int64, example: 3 }

    Tag:
      type: object
      required: [name, color, message_count]
      properties:
        name: { type: string, example: billing }
        color: { type: string, description: 'A CSS color, or empty for the automatic color.', example: '#2563eb' }
        message_count: { type: integer, example: 4 }
    TagRule:
      type: object
      required: [id, name, field, pattern, tags, active, created_at]
      properties:
        id: { type: string, example: trule_ai6monxoyc6e4hcv }
        workspace_id: { type: string, example: ws_k3m9x2p7qa4d8fbn }
        inbox_id: { type: string, nullable: true }
        name: { type: string, example: Billing emails }
        field: { type: string, enum: [from, to, subject, header, all] }
        pattern: { type: string, description: A regular expression., example: 'billing@' }
        tags:
          type: array
          items: { type: string }
        active: { type: boolean }
        created_at: { type: string, format: date-time }
    TagRuleInput:
      type: object
      required: [name, field, pattern, tags]
      properties:
        inbox_id: { type: string, description: Omit to apply to every inbox. }
        name: { type: string, example: Billing emails }
        field: { type: string, enum: [from, to, subject, header, all], example: from }
        pattern: { type: string, example: 'billing@' }
        tags:
          type: array
          items: { type: string }
        active: { type: boolean, default: true }

    Webhook:
      type: object
      required: [id, url, created_at]
      properties:
        id: { type: string, example: wh_4mq7h2b9x1k3d8ra }
        inbox_id: { type: string, nullable: true }
        url: { type: string, format: uri, example: 'https://ci.example.com/hooks/email' }
        created_at: { type: string, format: date-time }
    WebhookCreated:
      allOf:
        - $ref: '#/components/schemas/Webhook'
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: Used to sign deliveries. Shown once.
    Delivery:
      type: object
      required: [id, message_id, event, status, attempts, created_at]
      properties:
        id: { type: integer, format: int64, example: 8412 }
        message_id: { type: string, example: msg_htumcicppd26jek6 }
        event: { type: string, example: message.received }
        status: { type: string, enum: [pending, delivered, failed] }
        attempts: { type: integer, example: 1 }
        last_status: { type: integer, nullable: true, description: The last HTTP status your endpoint returned., example: 200 }
        last_error: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time, nullable: true }
        next_attempt_at: { type: string, format: date-time, nullable: true }

    HTMLCheck:
      type: object
      description: Field names are capitalised.
      properties:
        Warnings:
          type: array
          items:
            type: object
            properties:
              Slug: { type: string }
              Title: { type: string }
              Description: { type: string }
              URL: { type: string }
              Category: { type: string }
              Tags:
                type: array
                items: { type: string }
              Results:
                type: array
                items:
                  type: object
                  properties:
                    Name: { type: string }
                    Platform: { type: string }
                    Family: { type: string }
                    Version: { type: string }
                    Support: { type: string, enum: [yes, no, partial] }
              Score:
                type: object
                properties:
                  Found: { type: integer }
                  Supported: { type: integer }
                  Partial: { type: integer }
                  Unsupported: { type: integer }
        Platforms:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
        Total:
          type: object
          properties:
            Tests: { type: integer }
            Nodes: { type: integer }
            Supported: { type: number }
            Partial: { type: number }
            Unsupported: { type: number }
    LinkCheck:
      type: object
      description: Field names are capitalised.
      properties:
        Links:
          type: array
          items:
            type: object
            properties:
              URL: { type: string }
              TargetURL: { type: string }
              StatusCode: { type: integer }
              Status: { type: string }
              LatencyMs: { type: integer }
        Errors: { type: integer, description: How many links did not answer successfully., example: 1 }
    SpamCheck:
      type: object
      description: Field names are capitalised.
      properties:
        IsSpam: { type: boolean }
        Score: { type: number }
        Rules:
          type: array
          items:
            type: object
            properties:
              Score: { type: number }
              Name: { type: string }
              Description: { type: string }
        Error: { type: string }

    ChaosTrigger:
      type: object
      required: [error_code, probability]
      properties:
        error_code:
          type: integer
          description: The SMTP reply code to fail with, for example 451 or 550.
          example: 451
        probability:
          type: integer
          minimum: 0
          maximum: 100
          description: Percent of attempts that fail.
    Chaos:
      type: object
      required: [enabled, sender, recipient, authentication]
      properties:
        id: { type: string }
        workspace_id: { type: string }
        inbox_id: { type: string, nullable: true }
        enabled: { type: boolean }
        sender: { $ref: '#/components/schemas/ChaosTrigger' }
        recipient: { $ref: '#/components/schemas/ChaosTrigger' }
        authentication: { $ref: '#/components/schemas/ChaosTrigger' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ChaosOutcome:
      type: object
      properties:
        triggered: { type: boolean }
        code: { type: integer }
        message: { type: string }
    ChaosInput:
      type: object
      properties:
        enabled: { type: boolean }
        sender: { $ref: '#/components/schemas/ChaosTrigger' }
        recipient: { $ref: '#/components/schemas/ChaosTrigger' }
        authentication: { $ref: '#/components/schemas/ChaosTrigger' }
