openapi: 3.1.0
info:
  title: myagentmail API
  version: "1.0.0"
  description: |
    myagentmail is a multi-tenant email API for AI agents.

    Create inboxes on the default `@myagentmail.com` domain or your own custom
    domains, send and receive mail, manage drafts, subscribe to real-time
    events over WebSocket, and isolate resources across workspaces so your
    reseller customers can serve their own end-customers under a single
    billing relationship.

    Install the TypeScript SDK: `npm install myagentmail`
    ([npm](https://www.npmjs.com/package/myagentmail))

    MCP server for Claude Desktop / Cursor: `npx -y myagentmail-mcp`
    ([npm](https://www.npmjs.com/package/myagentmail-mcp))

    ## Authentication

    Every request is authenticated with the `X-API-Key` header (or
    `Authorization: Bearer <key>`). Four key prefixes, each with increasing
    scope:

    | Prefix   | Scope                                   | Use it for                              |
    |----------|-----------------------------------------|-----------------------------------------|
    | `sa_...` | Super admin                             | Tenant lifecycle (platform operator)    |
    | `tk_...` | Tenant master                           | Everything in your tenant across workspaces |
    | `wk_...` | Workspace master                        | One workspace only                      |
    | `ak_...` | Inbox (agent) scoped                    | One inbox — its messages, drafts, lists |

    ## Resource hierarchy

    ```
    Tenant (billing root)
    └── Workspace  (isolation container)
        ├── Inboxes
        │   ├── Messages / Threads / Attachments
        │   ├── Drafts
        │   └── Lists
        └── Domains
    ```

    ## Sending mail

    Mail is sent via `POST /v1/inboxes/{id}/send`. Replies go through
    `/reply/{messageId}`, reply-all through `/reply-all/{messageId}`, and
    forwards through `/forward/{messageId}`. The `From` header always uses
    the inbox's **primary alias** — add a custom-domain address with
    `POST /v1/inboxes/{id}/addresses` to change it.

    ## Receiving mail

    Inbound mail arrives via SMTP on the default domain or any verified
    custom domain. Subscribe to delivery events either by registering a
    webhook (`POST /v1/webhooks`) or by opening a WebSocket
    (`wss://myagentmail.com/v1/ws`) — useful for in-line 2FA flows where
    the agent doesn't have a public webhook receiver.

servers:
  - url: https://myagentmail.com
    description: Production

tags:
  - name: Workspaces
    description: Isolated containers under a tenant. Hand scoped keys to your resellers so their customers cannot see each other.
  - name: Inboxes
    description: Email mailboxes that can send, receive, and store threads.
  - name: Addresses
    description: Multiple accepted addresses per inbox. Add a custom-domain alias to change the outbound From without losing thread history.
  - name: Messages
    description: Send, reply, forward, list, and download messages.
  - name: Drafts
    description: Compose messages across multiple agent turns and send atomically.
  - name: Lists
    description: Named groups of recipient addresses per inbox.
  - name: Domains
    description: Bring your own sending domain. Returns DNS records, verification state, and a BIND zone file export.
  - name: Webhooks
    description: HTTP callbacks fired on every inbox event.
  - name: WebSocket
    description: Real-time event stream. The agentmail.to spec-compatible wire protocol.
  - name: Metrics
    description: Programmatic counters scoped by workspace or inbox.
  - name: Tenants
    description: Super-admin only — platform operator endpoints.

security:
  - apiKeyHeader: []

components:
  securitySchemes:
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Any of `sa_`, `tk_`, `wk_`, `ak_`, or `mam_` keys. Scope is enforced
        per-endpoint — see individual operation descriptions.
  parameters:
    InboxId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Inbox UUID
    WorkspaceId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Workspace UUID
    MessageId:
      name: messageId
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Message UUID
    DraftId:
      name: did
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Draft UUID
    ListId:
      name: lid
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: List UUID
    AttachmentId:
      name: aid
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Attachment UUID
    DomainName:
      name: domain
      in: path
      required: true
      schema: { type: string, example: "mail.example.com" }
      description: Fully-qualified domain name
    LimitQuery:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    OffsetQuery:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, default: 0 }
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ValidationError:
      description: Request body failed schema validation
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: Resource already exists
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string, example: "VALIDATION_ERROR" }
      required: [error]

    Workspace:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string, example: "Recruiter Corp" }
        slug: { type: string, example: "recruiter-corp" }
        isDefault: { type: boolean }
        inboxCount: { type: integer }
        domainCount: { type: integer }
        createdAt: { type: string, format: date-time }
      required: [id, name, slug, isDefault]

    WorkspaceKey:
      type: object
      properties:
        id: { type: string, format: uuid }
        key:
          type: string
          description: Plain-text key — only returned on creation. Store it somewhere safe.
          example: "wk_95b6819dfa92362d99a26b947bf302d864043865b892c9d9"
        prefix: { type: string, example: "wk_95b68" }
        name: { type: string }
        workspaceId: { type: string, format: uuid }
        createdAt: { type: string, format: date-time }

    Inbox:
      type: object
      properties:
        id: { type: string, format: uuid }
        email:
          type: string
          description: Current primary outbound address — may be a custom-domain alias.
          example: "scout-abc123@delegated.cc"
        canonicalEmail:
          type: string
          description: Stable identity used for IMAP/SMTP auth. Never changes after creation.
          example: "scout-abc123-10deac42@myagentmail.com"
        username: { type: string }
        domain: { type: string }
        displayName: { type: string }
        workspaceId: { type: string, format: uuid }
        addresses:
          type: array
          items: { $ref: "#/components/schemas/InboxAddress" }
        imapPassword:
          type: string
          description: Only returned on creation and GET /inboxes/{id}. Stored plaintext so IMAP clients can log in.
        imap: { $ref: "#/components/schemas/ImapCreds" }
        smtp: { $ref: "#/components/schemas/SmtpCreds" }
        createdAt: { type: string, format: date-time }

    InboxAddress:
      type: object
      properties:
        address: { type: string, example: "scout-abc123@delegated.cc" }
        isPrimary: { type: boolean }

    ImapCreds:
      type: object
      properties:
        host: { type: string, example: "imap.myagentmail.com" }
        port: { type: integer, example: 993 }
        tls: { type: boolean }
        username: { type: string }
        password: { type: string }

    SmtpCreds:
      type: object
      properties:
        host: { type: string, example: "smtp.myagentmail.com" }
        port: { type: integer, example: 587 }
        starttls: { type: boolean }
        username: { type: string }
        password: { type: string }

    Message:
      type: object
      properties:
        id: { type: string, format: uuid }
        direction: { type: string, enum: [inbound, outbound] }
        from: { type: string }
        to: { type: string }
        subject: { type: string }
        plainBody: { type: string, nullable: true }
        htmlBody: { type: string, nullable: true }
        isRead: { type: boolean }
        threadId: { type: string, format: uuid, nullable: true }
        messageIdHeader: { type: string, nullable: true }
        inReplyTo: { type: string, nullable: true }
        receivedAt: { type: string, format: date-time }
        attachments:
          type: array
          items: { $ref: "#/components/schemas/AttachmentMeta" }

    AttachmentMeta:
      type: object
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        contentType: { type: string, example: "application/pdf" }
        sizeBytes: { type: integer }

    Thread:
      type: object
      properties:
        threadId: { type: string, format: uuid }
        subject: { type: string }
        messageCount: { type: integer }
        firstMessageAt: { type: string, format: date-time }
        lastMessageAt: { type: string, format: date-time }

    Draft:
      type: object
      properties:
        id: { type: string, format: uuid }
        inboxId: { type: string, format: uuid }
        to: { type: array, items: { type: string, format: email } }
        cc: { type: array, items: { type: string, format: email } }
        bcc: { type: array, items: { type: string, format: email } }
        subject: { type: string }
        plainBody: { type: string, nullable: true }
        htmlBody: { type: string, nullable: true }
        replyToMessageId: { type: string, format: uuid, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    List:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        entryCount: { type: integer }
        entries:
          type: array
          items: { $ref: "#/components/schemas/ListEntry" }
        createdAt: { type: string, format: date-time }

    ListEntry:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        displayName: { type: string, nullable: true }
        metadata: { type: object, additionalProperties: true, nullable: true }
        createdAt: { type: string, format: date-time }

    Domain:
      type: object
      properties:
        id: { type: string, format: uuid }
        domain: { type: string }
        status: { type: string, enum: [pending, verified, failed] }
        workspaceId: { type: string, format: uuid }
        dnsRecords:
          type: array
          items: { $ref: "#/components/schemas/DnsRecord" }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    DnsRecord:
      type: object
      properties:
        type: { type: string, enum: [TXT, CNAME, MX, A] }
        name: { type: string }
        value: { type: string }
        priority: { type: integer, nullable: true }
        status: { type: string, enum: [pending, verified], nullable: true }

    Webhook:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string, enum: [message.received, message.sent, message.bounced] }
        secret:
          type: string
          description: HMAC-SHA256 secret. Only returned on creation.
        isActive: { type: boolean }
        createdAt: { type: string, format: date-time }

    MetricsResponse:
      type: object
      properties:
        window:
          type: object
          properties:
            from: { type: string, format: date-time }
            to: { type: string, format: date-time }
        scope:
          type: object
          properties:
            tenantId: { type: string, format: uuid }
            workspaceId: { type: string, format: uuid, nullable: true }
            inboxId: { type: string, format: uuid, nullable: true }
        counts:
          type: object
          properties:
            sent: { type: integer }
            received: { type: integer }
            bounced: { type: integer }
        resources:
          type: object
          properties:
            inboxes: { type: integer }
            domains: { type: integer }

paths:
  # ────────────────────────────────────────────────────────────────
  # WORKSPACES
  # ────────────────────────────────────────────────────────────────
  /v1/workspaces:
    post:
      tags: [Workspaces]
      summary: Create a workspace
      description: Requires a tenant master key (`tk_...`). Workspaces are isolation containers — inboxes and domains created in one cannot be seen from another.
      operationId: createWorkspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: "Recruiter Corp" }
                slug: { type: string, example: "recruiter-corp" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Workspace" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [Workspaces]
      summary: List workspaces
      operationId: listWorkspaces
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  workspaces:
                    type: array
                    items: { $ref: "#/components/schemas/Workspace" }

  /v1/workspaces/{id}:
    parameters: [ { $ref: "#/components/parameters/WorkspaceId" } ]
    get:
      tags: [Workspaces]
      summary: Get a workspace
      operationId: getWorkspace
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Workspace" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Workspaces]
      summary: Delete a workspace
      description: Cascades to all inboxes, domains, and scoped keys. Cannot delete the tenant's default workspace.
      operationId: deleteWorkspace
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean }
                  id: { type: string, format: uuid }

  /v1/workspaces/{id}/keys:
    parameters: [ { $ref: "#/components/parameters/WorkspaceId" } ]
    post:
      tags: [Workspaces]
      summary: Issue a workspace master key
      description: Returns a `wk_...` key scoped to this workspace. The key is only shown in the response body — store it securely.
      operationId: createWorkspaceKey
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkspaceKey" }
    get:
      tags: [Workspaces]
      summary: List workspace master keys
      operationId: listWorkspaceKeys
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        prefix: { type: string }
                        name: { type: string }
                        createdAt: { type: string, format: date-time }

  /v1/workspaces/{id}/keys/{key_id}:
    parameters:
      - { $ref: "#/components/parameters/WorkspaceId" }
      - name: key_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    delete:
      tags: [Workspaces]
      summary: Revoke a workspace master key
      operationId: revokeWorkspaceKey
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked: { type: boolean }
                  id: { type: string, format: uuid }

  # ────────────────────────────────────────────────────────────────
  # INBOXES
  # ────────────────────────────────────────────────────────────────
  /v1/inboxes:
    post:
      tags: [Inboxes]
      summary: Create an inbox
      description: |
        Requires tenant master or workspace master key. Tenant master keys target the default workspace unless `workspaceId` is set in the body. Returns IMAP/SMTP credentials — the `imapPassword` is only returned once.
      operationId: createInbox
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
                  description: Lowercase alphanumeric + `.-_`. Auto-generated if omitted. On the default domain a tenant-unique suffix is appended.
                displayName: { type: string }
                domain: { type: string, description: "Defaults to myagentmail.com" }
                workspaceId:
                  type: string
                  format: uuid
                  description: (Tenant master only) target workspace. Defaults to the tenant's default workspace.
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "403":
          description: Plan limit reached
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    get:
      tags: [Inboxes]
      summary: List inboxes
      operationId: listInboxes
      parameters:
        - name: workspace
          in: query
          description: Tenant master only — filter to a single workspace.
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  inboxes:
                    type: array
                    items: { $ref: "#/components/schemas/Inbox" }

  /v1/inboxes/{id}:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    get:
      tags: [Inboxes]
      summary: Get an inbox
      description: Returns the inbox detail including IMAP/SMTP credentials and the full address list.
      operationId: getInbox
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Inboxes]
      summary: Update inbox display name or metadata
      description: Username, domain, and canonical identity are immutable. Use `POST /v1/inboxes/{id}/addresses` to change the outbound From.
      operationId: updateInbox
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName: { type: string }
                metadata: { type: object, additionalProperties: true }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated: { type: boolean }
                  id: { type: string, format: uuid }
    delete:
      tags: [Inboxes]
      summary: Delete an inbox
      description: Soft-deletes the inbox, hard-deletes its alias rows, and removes the Stalwart principal. Prior threads and messages are preserved but hidden from listings.
      operationId: deleteInbox
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean }
                  id: { type: string, format: uuid }

  /v1/inboxes/{id}/addresses:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    post:
      tags: [Addresses]
      summary: Add an alias address
      description: |
        Attach another accepted address to this inbox. If `primary: true` (the default), it also becomes the outbound `From`. The domain must be either the default domain or a custom domain registered + verified to the same workspace.
      operationId: addInboxAddress
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email, example: "scout-abc123@delegated.cc" }
                primary: { type: boolean, default: true }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  address: { type: string }
                  isPrimary: { type: boolean }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/inboxes/{id}/addresses/{address}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - name: address
        in: path
        required: true
        schema: { type: string }
        description: URL-encoded alias email
    delete:
      tags: [Addresses]
      summary: Remove an alias address
      description: Cannot remove the canonical (initial) address. If the removed alias was primary, the canonical address becomes primary again.
      operationId: removeInboxAddress
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean }
                  address: { type: string }

  # ────────────────────────────────────────────────────────────────
  # MESSAGES
  # ────────────────────────────────────────────────────────────────
  /v1/inboxes/{id}/send:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    post:
      tags: [Messages]
      summary: Send a message
      description: |
        Sends from this inbox. The `From` header is derived from the inbox's primary alias. Pass `verified: true` when you've validated the recipient domain out-of-band (e.g. via verify-email); otherwise, sends to an address that has never replied to this inbox are rejected as a deliverability safeguard.
      operationId: sendMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, subject]
              properties:
                to:
                  oneOf:
                    - { type: string, format: email }
                    - { type: array, items: { type: string, format: email } }
                subject: { type: string }
                plainBody: { type: string }
                htmlBody: { type: string }
                replyTo: { type: string, format: email }
                verified: { type: boolean, default: false }
      responses:
        "200":
          description: Sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  threadId: { type: string, format: uuid }
                  status: { type: string, example: "sent" }
                  messageId: { type: string }
        "403": { description: "Inbox paused or plan limit reached" }
        "502": { description: "SMTP relay error" }

  /v1/inboxes/{id}/reply/{messageId}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
    post:
      tags: [Messages]
      summary: Reply to a message
      description: Replies preserve the thread (In-Reply-To + References) and the original recipient.
      operationId: replyMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                plainBody: { type: string }
                htmlBody: { type: string }
      responses:
        "200": { description: Sent }

  /v1/inboxes/{id}/reply-all/{messageId}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
    post:
      tags: [Messages]
      summary: Reply-all to a message
      description: Sends to the original sender plus every address in the original To list, with any of this inbox's own addresses filtered out.
      operationId: replyAllMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                plainBody: { type: string }
                htmlBody: { type: string }
      responses:
        "200": { description: Sent }

  /v1/inboxes/{id}/forward/{messageId}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
    post:
      tags: [Messages]
      summary: Forward a message
      description: Prepends a "Forwarded message" header block and sends the combined body.
      operationId: forwardMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to]
              properties:
                to:
                  oneOf:
                    - { type: string, format: email }
                    - { type: array, items: { type: string, format: email } }
                subject: { type: string }
                plainBody: { type: string }
                htmlBody: { type: string }
      responses:
        "200": { description: Sent }

  /v1/inboxes/{id}/threads:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    get:
      tags: [Messages]
      summary: List threads
      operationId: listThreads
      parameters:
        - { $ref: "#/components/parameters/LimitQuery" }
        - { $ref: "#/components/parameters/OffsetQuery" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  threads:
                    type: array
                    items: { $ref: "#/components/schemas/Thread" }

  /v1/inboxes/{id}/threads/{threadId}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - name: threadId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Messages]
      summary: Get a thread with all messages
      operationId: getThread
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  threadId: { type: string, format: uuid }
                  messageCount: { type: integer }
                  messages:
                    type: array
                    items: { $ref: "#/components/schemas/Message" }

  /v1/inboxes/{id}/messages:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    get:
      tags: [Messages]
      summary: List messages
      operationId: listMessages
      parameters:
        - { $ref: "#/components/parameters/LimitQuery" }
        - { $ref: "#/components/parameters/OffsetQuery" }
        - name: direction
          in: query
          schema: { type: string, enum: [inbound, outbound] }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items: { $ref: "#/components/schemas/Message" }
                  pagination:
                    type: object
                    properties:
                      total: { type: integer }
                      limit: { type: integer }
                      offset: { type: integer }

  /v1/inboxes/{id}/messages/{messageId}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
    get:
      tags: [Messages]
      summary: Get a message
      description: Returns full bodies plus attachment metadata. Also marks the message as read.
      operationId: getMessage
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
    patch:
      tags: [Messages]
      summary: Update a message
      description: Mark read/unread.
      operationId: updateMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                isRead: { type: boolean }
      responses:
        "200": { description: Updated }
    delete:
      tags: [Messages]
      summary: Soft-delete a message
      description: Sets `deleted_at`. The row stays so thread history doesn't renumber; list queries filter deleted messages out.
      operationId: deleteMessage
      responses:
        "200": { description: Deleted }

  /v1/inboxes/{id}/messages/{messageId}/raw:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
    get:
      tags: [Messages]
      summary: Download raw .eml source
      description: Returns the full RFC 5322 source as `message/rfc822`. Only available for inbound messages; outbound messages do not persist their raw source.
      operationId: getMessageRaw
      responses:
        "200":
          description: Raw email source
          content:
            message/rfc822:
              schema: { type: string, format: binary }
        "404": { description: "Raw source not available" }

  /v1/inboxes/{id}/messages/{messageId}/attachments:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
    get:
      tags: [Messages]
      summary: List attachments on a message
      operationId: listAttachments
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  attachments:
                    type: array
                    items: { $ref: "#/components/schemas/AttachmentMeta" }

  /v1/inboxes/{id}/messages/{messageId}/attachments/{aid}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/MessageId" }
      - { $ref: "#/components/parameters/AttachmentId" }
    get:
      tags: [Messages]
      summary: Download an attachment
      description: Streams the raw attachment bytes with the original `Content-Type` and `Content-Disposition` headers.
      operationId: getAttachment
      responses:
        "200":
          description: Attachment bytes
          content:
            application/octet-stream:
              schema: { type: string, format: binary }

  # ────────────────────────────────────────────────────────────────
  # DRAFTS
  # ────────────────────────────────────────────────────────────────
  /v1/inboxes/{id}/drafts:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    post:
      tags: [Drafts]
      summary: Create a draft
      operationId: createDraft
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                to:
                  oneOf:
                    - { type: string, format: email }
                    - { type: array, items: { type: string, format: email } }
                cc:
                  oneOf:
                    - { type: string, format: email }
                    - { type: array, items: { type: string, format: email } }
                bcc:
                  oneOf:
                    - { type: string, format: email }
                    - { type: array, items: { type: string, format: email } }
                subject: { type: string }
                plainBody: { type: string }
                htmlBody: { type: string }
                replyToMessageId: { type: string, format: uuid }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Draft" }
    get:
      tags: [Drafts]
      summary: List drafts
      operationId: listDrafts
      parameters:
        - { $ref: "#/components/parameters/LimitQuery" }
        - { $ref: "#/components/parameters/OffsetQuery" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  drafts:
                    type: array
                    items: { $ref: "#/components/schemas/Draft" }

  /v1/inboxes/{id}/drafts/{did}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/DraftId" }
    get:
      tags: [Drafts]
      summary: Get a draft
      operationId: getDraft
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Draft" }
    patch:
      tags: [Drafts]
      summary: Update a draft
      description: Merge-patch. Omitted fields are left as-is.
      operationId: updateDraft
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Draft" }
      responses:
        "200": { description: Updated }
    delete:
      tags: [Drafts]
      summary: Delete a draft
      operationId: deleteDraft
      responses:
        "200": { description: Deleted }

  /v1/inboxes/{id}/drafts/{did}/send:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/DraftId" }
    post:
      tags: [Drafts]
      summary: Send a draft
      description: Sends the current draft contents and deletes the draft atomically. If the draft has `replyToMessageId`, the sent message is threaded as a reply.
      operationId: sendDraft
      responses:
        "200":
          description: Sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  threadId: { type: string, format: uuid }
                  status: { type: string }
                  messageId: { type: string }
                  draftId: { type: string, format: uuid }

  # ────────────────────────────────────────────────────────────────
  # LISTS
  # ────────────────────────────────────────────────────────────────
  /v1/inboxes/{id}/lists:
    parameters: [ { $ref: "#/components/parameters/InboxId" } ]
    get:
      tags: [Lists]
      summary: List lists
      operationId: listLists
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  lists:
                    type: array
                    items: { $ref: "#/components/schemas/List" }
    post:
      tags: [Lists]
      summary: Create a list
      operationId: createList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/List" }

  /v1/inboxes/{id}/lists/{lid}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/ListId" }
    get:
      tags: [Lists]
      summary: Get a list with entries
      operationId: getList
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/List" }
    delete:
      tags: [Lists]
      summary: Delete a list
      operationId: deleteList
      responses:
        "200": { description: Deleted }

  /v1/inboxes/{id}/lists/{lid}/entries:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/ListId" }
    post:
      tags: [Lists]
      summary: Add a list entry
      operationId: addListEntry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                displayName: { type: string }
                metadata: { type: object, additionalProperties: true }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListEntry" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/inboxes/{id}/lists/{lid}/entries/{eid}:
    parameters:
      - { $ref: "#/components/parameters/InboxId" }
      - { $ref: "#/components/parameters/ListId" }
      - name: eid
        in: path
        required: true
        schema: { type: string, format: uuid }
    delete:
      tags: [Lists]
      summary: Remove a list entry
      operationId: removeListEntry
      responses:
        "200": { description: Deleted }

  # ────────────────────────────────────────────────────────────────
  # DOMAINS
  # ────────────────────────────────────────────────────────────────
  /v1/domains:
    post:
      tags: [Domains]
      summary: Register a custom domain
      description: Returns the DNS records you need to set at your registrar. Domain starts in `pending` status until you call the verify endpoint.
      operationId: createDomain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string, example: "mail.example.com" }
                workspaceId:
                  type: string
                  format: uuid
                  description: (Tenant master only) target workspace.
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Domain" }
    get:
      tags: [Domains]
      summary: List domains
      operationId: listDomains
      parameters:
        - name: workspace
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items: { $ref: "#/components/schemas/Domain" }

  /v1/domains/{domain}:
    parameters: [ { $ref: "#/components/parameters/DomainName" } ]
    get:
      tags: [Domains]
      summary: Get a domain
      operationId: getDomain
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Domain" }
    delete:
      tags: [Domains]
      summary: Delete a domain
      operationId: deleteDomain
      responses:
        "200": { description: Deleted }

  /v1/domains/{domain}/verify:
    parameters: [ { $ref: "#/components/parameters/DomainName" } ]
    get:
      tags: [Domains]
      summary: Verify a domain
      description: Re-checks DNS records with the upstream ESP. If the domain is now verified, Mission Control atomically adds the new address as a primary alias on the inbox (preserving thread history).
      operationId: verifyDomain
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: { type: string }
                  status: { type: string, enum: [pending, verified, failed] }
                  verified: { type: boolean }
                  dnsRecords:
                    type: array
                    items: { $ref: "#/components/schemas/DnsRecord" }

  /v1/domains/{domain}/zone-file:
    parameters: [ { $ref: "#/components/parameters/DomainName" } ]
    get:
      tags: [Domains]
      summary: Export BIND zone file
      description: Returns the current DNS records as a BIND-format zone file fragment. Convenient for pasting into a DNS editor that accepts zone-file imports.
      operationId: getDomainZoneFile
      responses:
        "200":
          description: Zone file text
          content:
            text/plain:
              schema: { type: string }

  # ────────────────────────────────────────────────────────────────
  # WEBHOOKS
  # ────────────────────────────────────────────────────────────────
  /v1/webhooks:
    post:
      tags: [Webhooks]
      summary: Create a webhook
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string, enum: [message.received, message.sent, message.bounced] }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
    get:
      tags: [Webhooks]
      summary: List webhooks
      operationId: listWebhooks
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items: { $ref: "#/components/schemas/Webhook" }

  /v1/webhooks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Webhooks]
      summary: Get a webhook
      operationId: getWebhook
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
    patch:
      tags: [Webhooks]
      summary: Update a webhook
      description: Change url, events, or enabled state.
      operationId: updateWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string, enum: [message.received, message.sent, message.bounced] }
                isActive: { type: boolean }
      responses:
        "200": { description: Updated }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      operationId: deleteWebhook
      responses:
        "200": { description: Deleted }

  # ────────────────────────────────────────────────────────────────
  # METRICS
  # ────────────────────────────────────────────────────────────────
  /v1/metrics:
    get:
      tags: [Metrics]
      summary: Query metrics
      description: Returns counts (sent, received, bounced) and resource totals (inboxes, domains) for a time window. Workspace master keys are locked to their own workspace; tenant master keys can scope with query params.
      operationId: getMetrics
      parameters:
        - name: from
          in: query
          schema: { type: string, format: date-time }
          description: ISO-8601 or epoch seconds/ms. Defaults to 30 days ago.
        - name: to
          in: query
          schema: { type: string, format: date-time }
          description: ISO-8601 or epoch seconds/ms. Defaults to now.
        - name: workspace
          in: query
          schema: { type: string, format: uuid }
        - name: inbox
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetricsResponse" }

  # ────────────────────────────────────────────────────────────────
  # WEBSOCKET
  # ────────────────────────────────────────────────────────────────
  /v1/ws:
    get:
      tags: [WebSocket]
      summary: Open a WebSocket connection
      description: |
        Upgrade this GET to a WebSocket. After the handshake, the server
        sends a `connected` frame and the client sends a `subscribe`
        frame to start receiving events.

        This protocol is wire-compatible with agentmail.to's WebSocket
        API — `workspace_ids` also accepts `pod_ids` as an alias.

        ### Client → server frames

        ```json
        { "type": "subscribe",
          "event_types": ["message.received", "message.sent"],
          "inbox_ids":    ["..."],
          "workspace_ids": ["..."] }
        ```

        ### Server → client frames

        ```json
        { "type": "connected", "inboxId": "...", "email": "..." }
        { "type": "subscribed", "event_types": [...], "inbox_ids": [...], "workspace_ids": [...] }
        { "type": "event",
          "event_type": "message.received",
          "event_id":   "evt_...",
          "message":    { "inbox_id": "...", "thread_id": "...", "from": "...", "to": ["..."], "subject": "...", "plain_body": "...", "html_body": "...", "timestamp": "..." },
          "thread":     { "thread_id": "...", "subject": "..." } }
        { "type": "ping" }
        { "type": "error", "message": "..." }
        ```

        Use `?api_key=<key>` as a query parameter when your client
        (e.g. browser `new WebSocket`) can't set the `X-API-Key`
        header.
      operationId: openWebSocket
      responses:
        "101": { description: Switching protocols }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ────────────────────────────────────────────────────────────────
  # UTILITIES
  # ────────────────────────────────────────────────────────────────
  /v1/verify-email:
    post:
      tags: [Messages]
      summary: Check recipient deliverability
      description: "MX-probe the recipient domain(s) to filter invalid addresses before sending. Use in combination with `verified: true` on the send endpoint."
      operationId: verifyEmail
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
                emails:
                  type: array
                  items: { type: string, format: email }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  validAddresses: { type: array, items: { type: string } }
                  invalidAddresses: { type: array, items: { type: string } }

  # ────────────────────────────────────────────────────────────────
  # TENANTS (super admin)
  # ────────────────────────────────────────────────────────────────
  /v1/tenants:
    post:
      tags: [Tenants]
      summary: Create a tenant
      description: Super admin only. Creates a new billing root and auto-provisions its Default workspace.
      operationId: createTenant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "201": { description: Created }
    get:
      tags: [Tenants]
      summary: List tenants
      operationId: listTenants
      responses:
        "200": { description: OK }

  /v1/tenants/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    delete:
      tags: [Tenants]
      summary: Delete a tenant
      description: Super admin only. Cascades to everything.
      operationId: deleteTenant
      responses:
        "200": { description: Deleted }
