openapi: 3.1.0
info:
  title: Mailwurf API
  version: 0.1.0
  summary: Isolated inboxes for E2E tests
  description: |
    Public mailbox API for automated tests. The app under test keeps its real
    mail sender. Tests create a temporary inbox, use the returned address as
    the recipient, and wait for the message over HTTP.

    `/api/v1` is an alias of `/v1`. `/health` is an alias of `/v1/health`.
    `/llm` and `/llms.txt` return this contract as Markdown and stay public.
servers:
  - url: https://api.mailwurf.io
    description: Mailwurf API
tags:
  - name: discovery
    x-displayName: Discovery
    description: Public guides and health. No API key.
  - name: inboxes
    x-displayName: Inboxes
    description: Temporary inboxes. One inbox per test.
  - name: messages
    x-displayName: Messages
    description: Parsed mail for an inbox. Wait is server-side; clients do not poll.
  - name: emails
    x-displayName: Emails
    description: Transactional mail sent by sender address, e.g. from an inbox on a sending domain.
security:
  - bearerAuth: []
  - apiKeyAuth: []
paths:
  /llm:
    get:
      operationId: getLlmGuide
      tags: [discovery]
      security: []
      summary: Agent guide
      description: Public Markdown API guide. Same body as `/llms.txt`.
      responses:
        "200":
          description: Markdown guide
          content:
            text/markdown:
              schema:
                type: string
  /llms.txt:
    get:
      operationId: getLlmsTxt
      tags: [discovery]
      security: []
      summary: Agent guide (llms.txt)
      description: Same Markdown body as `/llm`.
      responses:
        "200":
          description: Markdown guide
          content:
            text/markdown:
              schema:
                type: string
  /v1/health:
    get:
      operationId: getHealth
      tags: [discovery]
      security: []
      summary: API readiness
      description: Public JSON probe. `/health` is an alias of this path.
      responses:
        "200":
          description: Ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
              example:
                status: ok
  /v1/inboxes:
    post:
      operationId: createInbox
      tags: [inboxes]
      summary: Create a temporary inbox
      description: |
        Creates one isolated temporary inbox. Custom `address` is rejected.
        The returned `address` is `{id}@{mail domain}`. Expires after one hour.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInboxBody"
      responses:
        "201":
          description: Inbox created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      x-codeSamples:
        - lang: bash
          label: curl
          source: |
            curl -s -X POST https://api.mailwurf.io/v1/inboxes \
              -H "Authorization: Bearer $MAILWURF_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { createMailwurf } from "@mailwurf/e2e";
            const mailwurf = createMailwurf(process.env.MAILWURF_API_KEY!);
            const inbox = await mailwurf.createInbox();
    get:
      operationId: getInboxByAddress
      tags: [inboxes]
      summary: Look up an inbox by address
      parameters:
        - name: address
          in: query
          required: true
          description: Full recipient address, case-insensitive.
          schema:
            type: string
            example: inbox_abc@mail.mailwurf.io
      responses:
        "200":
          description: Inbox
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/inboxes/{id}:
    parameters:
      - $ref: "#/components/parameters/InboxId"
    get:
      operationId: getInbox
      tags: [inboxes]
      summary: Get an inbox
      description: Unknown ids or inboxes that do not belong to your API key return 404.
      responses:
        "200":
          description: Inbox
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteInbox
      tags: [inboxes]
      summary: Delete a temporary inbox
      description: Deletes the inbox and its messages. Permanent inboxes return 403.
      responses:
        "204":
          description: Deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/inboxes/{id}/messages:
    get:
      operationId: listMessages
      tags: [messages]
      summary: List messages
      description: Newest first. `subject` is a case-insensitive substring. `after` is a message id.
      parameters:
        - $ref: "#/components/parameters/InboxId"
        - $ref: "#/components/parameters/Subject"
        - $ref: "#/components/parameters/After"
      responses:
        "200":
          description: Messages
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: sendMessage
      tags: [messages]
      summary: Send from this inbox
      description: |
        `from` is the inbox's `sender_address` and cannot be set. Local recipients on the Mailwurf
        domain are delivered into that inbox so wait works. Internet recipients go out
        over SMTP from the mail worker.
      parameters:
        - $ref: "#/components/parameters/InboxId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendMessageBody"
      responses:
        "202":
          description: Queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendAccepted"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /v1/emails:
    post:
      operationId: sendEmail
      tags: [emails]
      summary: Send by sender address
      description: |
        Sends as `from`, the `address` or `sender_address` of an active inbox of the
        organization, optionally as `Name <address>`; the name goes into `From:`. For apps
        sending receipts and notifications (`@mailwurf/transactional`). Shares the send
        rate limit with sending from an inbox.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendEmailBody"
      responses:
        "202":
          description: Queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendAccepted"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /v1/inboxes/{id}/messages/wait:
    get:
      operationId: waitForMessage
      tags: [messages]
      summary: Wait for a message
      description: |
        Keep one request open. Do not poll. Matching is oldest-first.
        If a match is already stored, it returns immediately.
        At most four waits may run at once per API key.
      parameters:
        - $ref: "#/components/parameters/InboxId"
        - $ref: "#/components/parameters/Subject"
        - $ref: "#/components/parameters/After"
        - name: timeout
          in: query
          description: Milliseconds to wait. Default 15000. Max 120000. 0 is a single lookup.
          schema:
            type: integer
            minimum: 0
            maximum: 120000
            default: 15000
      responses:
        "200":
          description: Matching message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: timeout=0 and no matching message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: no matching message
        "408":
          description: Wait timed out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: timeout
        "429":
          description: Too many concurrent waits for this key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: too many concurrent waits
      x-codeSamples:
        - lang: bash
          label: curl
          source: |
            curl -s "https://api.mailwurf.io/v1/inboxes/{id}/messages/wait?timeout=15000&subject=Confirm" \
              -H "Authorization: Bearer $MAILWURF_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            const mail = await inbox.waitFor({ subject: "Confirm", timeout: 15_000 });
            await page.goto(mail.links[0]!);
  /v1/messages/{id}:
    get:
      operationId: getMessage
      tags: [messages]
      summary: Get one message
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Organization API key (`mw_live_…`).
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Same key as the bearer token.
  parameters:
    InboxId:
      name: id
      in: path
      required: true
      schema:
        type: string
        example: inbox_abc
    Subject:
      name: subject
      in: query
      required: false
      description: Case-insensitive substring of Subject.
      schema:
        type: string
        example: Confirm
    After:
      name: after
      in: query
      required: false
      description: Message id. Only later messages are returned.
      schema:
        type: string
  responses:
    BadRequest:
      description: Bad JSON, unknown field, missing query, or invalid after cursor
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or wrong API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: unauthorized
    Forbidden:
      description: Permanent inboxes are managed in the dashboard
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: permanent inboxes are managed in the dashboard
    NotFound:
      description: Unknown inbox or message, or one that does not belong to your API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: Too many concurrent waits or sends
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Health:
      type: object
      required: [status]
      properties:
        status:
          type: string
          example: ok
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
    CreateInboxBody:
      type: object
      additionalProperties: false
      properties: {}
      description: Empty object. Custom address is not supported.
    SendMessageBody:
      type: object
      additionalProperties: false
      required: [to, subject]
      properties:
        to:
          type: string
          example: other@mail.mailwurf.io
        subject:
          type: string
        text:
          type: string
        html:
          type: string
        in_reply_to:
          type: string
          description: Mailwurf message id to thread against
        attachments:
          type: array
          maxItems: 10
          description: Files to attach. The whole mail, attachments included, must stay under 5 MiB.
          items:
            $ref: "#/components/schemas/Attachment"
    SendEmailBody:
      type: object
      additionalProperties: false
      required: [from, to, subject]
      properties:
        from:
          type: string
          example: Acme Billing <billing@acme.com>
        to:
          type: string
          example: customer@example.com
        subject:
          type: string
        text:
          type: string
        html:
          type: string
        attachments:
          type: array
          maxItems: 10
          description: Files to attach. The whole mail, attachments included, must stay under 5 MiB.
          items:
            $ref: "#/components/schemas/Attachment"
    Attachment:
      type: object
      additionalProperties: false
      required: [filename, content]
      properties:
        filename:
          type: string
          example: invoice.pdf
        content:
          type: string
          format: byte
          description: File content, base64-encoded.
        content_type:
          type: string
          example: application/pdf
          description: Media type without parameters. Guessed from the file name when omitted.
    SendAccepted:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
          example: outbox_abc
        status:
          type: string
          example: queued
    Inbox:
      type: object
      required: [id, address, sender_address, kind, created_at, expires_at]
      properties:
        id:
          type: string
          example: inbox_abc
        address:
          type: string
          example: inbox_abc@mail.mailwurf.io
          description: Receives mail. On a sending domain `name@send.{domain}`.
        sender_address:
          type: string
          example: inbox_abc@mail.mailwurf.io
          description: "`From:` of mail the inbox sends: `address`, or `name@{domain}` on a sending domain."
        kind:
          type: string
          enum: [temporary, permanent]
        created_at:
          type: string
          format: date-time
        expires_at:
          type: [string, "null"]
          format: date-time
    Message:
      type: object
      required: [id, inbox_id, from, to, subject, text, html, links, otp, received_at]
      properties:
        id:
          type: string
        inbox_id:
          type: string
        from:
          type: string
          example: app@example.com
        to:
          type: string
          example: inbox_abc@mail.mailwurf.io
        subject:
          type: string
          example: Confirm your account
        text:
          type: string
        html:
          type: string
        links:
          type: array
          items:
            type: string
            format: uri
        otp:
          type: string
          description: Labeled 4–8 char code (digits, or uppercase letters with digits), else the first 6-digit word, else a 6-char code on its own line, else empty.
          example: "482193"
        received_at:
          type: string
          format: date-time
    MessageList:
      type: object
      required: [messages]
      properties:
        messages:
          type: array
          items:
            $ref: "#/components/schemas/Message"
