openapi: 3.0.3
info:
  title: Mailstorm API
  description: |
    Send transactional emails, manage domains, and process inbound mail through a single
    self-hostable platform. Mailstorm offers what Resend, SendGrid, and Postmark do — plus
    a built-in inbox, IMAP support, and the option to run it on your own infrastructure.

    ## Authentication
    All `/v1/*` endpoints use API key authentication via the `Authorization` header:
    ```
    Authorization: Bearer ms_live_xxxxxxxxxxxxxxxx
    ```

    Two key types are supported:
    - `ms_live_*` — sends real emails (requires verified account)
    - `ms_test_*` — captures emails without sending (sandbox mode)

    ## Rate Limits
    - 100 requests per minute per account on `/v1/*` endpoints
    - 10 requests per minute per IP on auth endpoints

    Exceeding the limit returns `429 Too Many Requests`.

    ## Idempotency
    POST endpoints (`/v1/emails`, `/v1/batch`) accept an `Idempotency-Key` header.
    Responses are cached for 24 hours per account+key. Retries with the same key return
    the original response without re-sending.

  version: '1.0.0'
  contact:
    name: Mailstorm
    url: https://mailstorm.dev
  license:
    name: MIT

servers:
  - url: https://mailstorm.dev
    description: Production
  - url: http://localhost:8080
    description: Local development

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: ms_live_xxx or ms_test_xxx
      description: |
        API key authentication. Generate keys from the dashboard at
        https://mailstorm.dev/api-keys.

  schemas:
    Email:
      type: object
      required: [from, to]
      properties:
        from:
          type: string
          example: "Acme <hello@acme.com>"
          description: Sender address. Must be from a verified domain.
        to:
          type: string
          example: "user@example.com"
          description: Recipient email address.
        subject:
          type: string
          example: "Welcome to Acme"
        html:
          type: string
          example: "<h1>Hello</h1><p>Welcome aboard!</p>"
        text:
          type: string
          example: "Hello\n\nWelcome aboard!"
        reply_to:
          type: string
          example: "support@acme.com"
        cc:
          type: array
          items:
            type: string
        bcc:
          type: array
          items:
            type: string
        headers:
          type: object
          additionalProperties:
            type: string
        template_id:
          type: string
          description: Optional template ID. If set, html/text/subject are rendered from template.
        variables:
          type: object
          additionalProperties:
            type: string
          description: Variables to substitute into the template (e.g., `{{name}}`).

    EmailResponse:
      type: object
      properties:
        id:
          type: string
          example: "a1b2c3d4e5f6..."
        status:
          type: string
          enum: [queued, captured, suppressed, sent, failed]
        message:
          type: string

    BatchRequest:
      type: object
      required: [emails]
      properties:
        emails:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/Email'

    BatchResponse:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              to:
                type: string
              status:
                type: string

    EmailEvent:
      type: object
      properties:
        type:
          type: string
          enum: [delivered, bounced, complained, opened, clicked]
        created_at:
          type: string
          format: date-time
        data:
          type: object

    Error:
      type: object
      properties:
        error:
          type: string
          example: "from and to are required"

paths:
  /v1/emails:
    post:
      summary: Send an email
      description: |
        Send a single transactional email. Returns immediately with a queued status — actual
        delivery happens asynchronously.

        **Idempotency:** Pass an `Idempotency-Key` header to safely retry on network errors.
        Responses are cached for 24 hours.
      operationId: sendEmail
      tags: [Emails]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
          description: Unique key to deduplicate retries.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Email'
            examples:
              simple:
                summary: Simple email
                value:
                  from: "Acme <hello@acme.com>"
                  to: "user@example.com"
                  subject: "Welcome"
                  html: "<h1>Welcome to Acme</h1>"
                  text: "Welcome to Acme"
              template:
                summary: Using a template
                value:
                  from: "hello@acme.com"
                  to: "user@example.com"
                  template_id: "tmpl_abc123"
                  variables:
                    name: "Alice"
                    plan: "Pro"
      responses:
        '202':
          description: Email accepted for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid API key
        '403':
          description: Account email not verified
        '429':
          description: Rate limit exceeded

  /v1/batch:
    post:
      summary: Send a batch of emails
      description: Send up to 100 emails in a single API call. Each email is processed independently.
      operationId: sendBatch
      tags: [Emails]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchRequest'
      responses:
        '202':
          description: Batch accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '400':
          description: Invalid request (e.g., more than 100 emails)
        '401':
          description: Missing or invalid API key
        '403':
          description: Account email not verified

  /v1/emails/{id}:
    get:
      summary: Get email status
      description: Retrieve the current status and metadata for a previously sent email.
      operationId: getEmail
      tags: [Emails]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Email details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  from:
                    type: string
                  to:
                    type: string
                  subject:
                    type: string
                  status:
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  sent_at:
                    type: string
                    format: date-time
        '404':
          description: Email not found

  /v1/emails/{id}/events:
    get:
      summary: Get email events
      description: |
        Retrieve all tracking events for an email: delivered, bounced, opened, clicked, complained.
        Events are returned in chronological order.
      operationId: getEmailEvents
      tags: [Emails]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: List of events
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/EmailEvent'
        '404':
          description: Email not found

  /v1/suppressions:
    get:
      summary: List suppressed addresses
      description: |
        Returns the list of email addresses on the account's suppression list. These addresses
        will be skipped on send. Auto-populated from hard bounces and spam complaints.
      operationId: listSuppressions
      tags: [Suppressions]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: reason
          in: query
          schema:
            type: string
            enum: [bounce, complaint, manual]
        - name: q
          in: query
          schema:
            type: string
          description: Filter by email substring
      responses:
        '200':
          description: Paginated suppression list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        email:
                          type: string
                        reason:
                          type: string
                        created_at:
                          type: string
                          format: date-time
                  pagination:
                    type: object

    post:
      summary: Add to suppression list
      description: |
        Add one or more email addresses to the suppression list. Pass `email` for a single
        address or `emails` for a batch (max 1000). Default reason is `manual`.
      operationId: addSuppression
      tags: [Suppressions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                emails:
                  type: array
                  items:
                    type: string
                  maxItems: 1000
                reason:
                  type: string
                  enum: [bounce, complaint, manual]
                  default: manual
            examples:
              single:
                value:
                  email: "user@example.com"
                  reason: "manual"
              bulk:
                value:
                  emails: ["a@x.com", "b@x.com", "c@x.com"]
                  reason: "manual"
      responses:
        '201':
          description: Addresses added
          content:
            application/json:
              schema:
                type: object
                properties:
                  added:
                    type: integer
                  requested:
                    type: integer
                  reason:
                    type: string

  /v1/suppressions/{email}:
    get:
      summary: Check if address is suppressed
      operationId: checkSuppression
      tags: [Suppressions]
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Address is suppressed
        '404':
          description: Address is not on the suppression list

    delete:
      summary: Remove from suppression list
      operationId: deleteSuppression
      tags: [Suppressions]
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Removed
        '404':
          description: Not in suppression list

tags:
  - name: Emails
    description: Send and track transactional emails
  - name: Suppressions
    description: Manage the per-account email blocklist
