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.
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 $MAILWURF_URL/v1/inboxes \
              -H "Authorization: Bearer $MAILWURF_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { createClient } from "@mailwurf/e2e";
            const mailwurf = createClient();
            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@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 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/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 "$MAILWURF_URL/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@mailwurf.io
        subject:
          type: string
        text:
          type: string
        html:
          type: string
        in_reply_to:
          type: string
          description: Mailwurf message id to thread against
    SendAccepted:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
          example: outbox_abc
        status:
          type: string
          example: queued
    Inbox:
      type: object
      required: [id, address, kind, created_at, expires_at]
      properties:
        id:
          type: string
          example: inbox_abc
        address:
          type: string
          example: inbox_abc@mailwurf.io
        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@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 digit code, else the first 6-digit word, 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"
