openapi: 3.1.0
info:
  title: PineMail
  version: 0.3.0
  summary: Transactional email API for EU organisations
  description: |
    Authenticate with a project API key (`Authorization: Bearer pm_live_...`).
    Keys may be full-access or restricted to the scopes listed under security.

    Public JSON uses snake_case. Errors are RFC 7807 problem+json.
    Send is asynchronous: `202` means queued. Poll `GET /v1/emails/{id}`.
    Repeat the same `Idempotency-Key` when retrying a POST.
    `from` accepts `addr@domain` or `Name <addr@domain>` and must use a
    verified domain on the project (or @pinemail.app). Provide `html` or `text`.

    Webhook signing and events: see /docs/webhooks.
  contact:
    email: contact@pinemail.app
servers:
  - url: https://pinemail.app
    description: Production — POST /v1/emails
  - url: /api
    description: Same instance at /api/v1
tags:
  - name: Emails
  - name: Domains
  - name: Mailboxes
  - name: Webhooks
  - name: Templates
  - name: Directory
paths:
  /v1/emails:
    post:
      tags: [Emails]
      operationId: sendEmail
      summary: Queue a transactional email
      security: [{ bearerAuth: [emails:send] }]
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 200 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendEmail"
            example:
              from: Acme <hello@pinemail.app>
              to: [ada@example.com]
              subject: Welcome
              html: "<p>Hello from your app.</p>"
              reply_to: support@example.com
      responses:
        "202":
          description: Queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueuedEmail"
        "400":
          $ref: "#/components/responses/Problem"
        "401":
          $ref: "#/components/responses/Problem"
        "403":
          $ref: "#/components/responses/Problem"
        "422":
          $ref: "#/components/responses/Problem"
    get:
      tags: [Emails]
      operationId: listEmails
      summary: List recent emails in the project
      security: [{ bearerAuth: [emails:read] }]
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: cursor
          schema: { type: string }
      responses:
        "200":
          description: Newest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/EmailSummary"
                  next_cursor:
                    type: string
                    nullable: true
  /v1/emails/{id}:
    get:
      tags: [Emails]
      operationId: getEmail
      summary: Fetch one email and its timeline
      security: [{ bearerAuth: [emails:read] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Email
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmailDetail"
        "404":
          $ref: "#/components/responses/Problem"
  /v1/domains:
    get:
      tags: [Domains]
      operationId: listDomains
      summary: List sending domains
      security: [{ bearerAuth: [domains:read] }]
      responses:
        "200":
          description: Domains
    post:
      tags: [Domains]
      operationId: createDomain
      summary: Start domain verification
      security: [{ bearerAuth: [domains:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string, example: mail.example.com }
                region: { type: string, example: eu-north-1 }
      responses:
        "201":
          description: Created, DNS records included
  /v1/domains/{id}:
    get:
      tags: [Domains]
      operationId: getDomain
      security: [{ bearerAuth: [domains:read] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Domain and DNS records
    delete:
      tags: [Domains]
      operationId: deleteDomain
      security: [{ bearerAuth: [domains:write] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "204":
          description: Deleted
        "400":
          description: Hosted PineMail domain cannot be removed
  /v1/domains/{id}/verify:
    post:
      tags: [Domains]
      operationId: verifyDomain
      summary: Recheck SPF, DKIM and MAIL FROM
      security: [{ bearerAuth: [domains:write] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Current verification flags
  /v1/mailboxes:
    get:
      tags: [Mailboxes]
      operationId: listMailboxes
      security: [{ bearerAuth: [mailboxes:read] }]
      responses:
        "200":
          description: Mailboxes
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Mailbox"
    post:
      tags: [Mailboxes]
      operationId: createMailbox
      security: [{ bearerAuth: [mailboxes:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMailbox"
      responses:
        "201":
          description: Created. `password` is present only when PineMail generated one.
  /v1/mailboxes/{id}:
    get:
      tags: [Mailboxes]
      operationId: getMailbox
      security: [{ bearerAuth: [mailboxes:read] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Mailbox
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Mailbox"
    patch:
      tags: [Mailboxes]
      operationId: updateMailbox
      security: [{ bearerAuth: [mailboxes:write] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateMailbox"
      responses:
        "200":
          description: Updated mailbox
    delete:
      tags: [Mailboxes]
      operationId: deleteMailbox
      security: [{ bearerAuth: [mailboxes:write] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "204":
          description: Deleted
  /v1/webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      security: [{ bearerAuth: [webhooks:read] }]
      responses:
        "200":
          description: Webhooks
    post:
      tags: [Webhooks]
      operationId: createWebhook
      security: [{ bearerAuth: [webhooks:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
      responses:
        "201":
          description: Created. `secret` is shown once.
  /v1/webhooks/{id}:
    get:
      tags: [Webhooks]
      operationId: getWebhook
      security: [{ bearerAuth: [webhooks:read] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Webhook plus recent deliveries
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      security: [{ bearerAuth: [webhooks:write] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                enabled: { type: boolean }
      responses:
        "200":
          description: Updated
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      security: [{ bearerAuth: [webhooks:write] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "204":
          description: Deleted
  /v1/webhooks/{id}/test:
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Send a signed webhook.test event now
      security: [{ bearerAuth: [webhooks:write] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Immediate delivery result
          content:
            application/json:
              schema:
                type: object
                properties:
                  http_status: { type: integer, nullable: true }
                  success: { type: boolean }
                  delivery_id: { type: string }
  /v1/templates:
    get:
      tags: [Templates]
      operationId: listTemplates
      security: [{ bearerAuth: [templates:read] }]
      responses:
        "200":
          description: Templates
  /v1/templates/{id}:
    get:
      tags: [Templates]
      operationId: getTemplate
      security: [{ bearerAuth: [templates:read] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Template
  /v1/directory:
    get:
      tags: [Directory]
      operationId: listDirectory
      security: [{ bearerAuth: [directory:read] }]
      responses:
        "200":
          description: People plus inbound and outbound shares
    post:
      tags: [Directory]
      operationId: addDirectoryPerson
      security: [{ bearerAuth: [directory:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                title: { type: string }
                organization: { type: string }
                notes: { type: string }
      responses:
        "201":
          description: Created
  /v1/directory/{id}:
    delete:
      tags: [Directory]
      operationId: deleteDirectoryPerson
      security: [{ bearerAuth: [directory:write] }]
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "204":
          description: Deleted
        "400":
          description: Mailbox-synced members cannot be deleted
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: pm_live
      description: |
        Project API key. Full-access keys satisfy every scope. Restricted keys
        must include the scope named on the operation.
  parameters:
    Id:
      in: path
      name: id
      required: true
      schema: { type: string }
  schemas:
    SendEmail:
      type: object
      required: [from, to]
      description: |
        Queue one transactional email. Provide `html` or `text` (or both).
        `from` must be on a verified project domain. Display names are kept
        (`Acme <hello@pinemail.app>`).
      properties:
        from:
          type: string
          examples: ["hello@pinemail.app", "Acme <hello@pinemail.app>"]
        to:
          type: array
          items: { type: string }
        cc:
          type: array
          items: { type: string }
        bcc:
          type: array
          items: { type: string }
        reply_to: { type: string }
        subject: { type: string }
        html: { type: string }
        text: { type: string }
        headers:
          type: object
          additionalProperties: { type: string }
        tags:
          type: array
          items: { type: string }
    QueuedEmail:
      type: object
      required: [id, status, created_at]
      properties:
        id: { type: string }
        status: { type: string, example: queued }
        created_at: { type: string, format: date-time }
    EmailSummary:
      type: object
      properties:
        id: { type: string }
        status: { type: string }
        subject: { type: string }
        from: { type: string }
        to:
          type: array
          items: { type: string }
        created_at: { type: string, format: date-time }
    EmailDetail:
      allOf:
        - $ref: "#/components/schemas/EmailSummary"
        - type: object
          properties:
            html: { type: string }
            text: { type: string }
            events:
              type: array
              items:
                type: object
    Mailbox:
      type: object
      properties:
        id: { type: string }
        domain_id: { type: string }
        address: { type: string }
        local_part: { type: string }
        domain: { type: string }
        display_name: { type: string, nullable: true }
        is_catch_all: { type: boolean }
        quota_mb: { type: integer }
        daily_send_limit: { type: integer, nullable: true }
        spam_level: { type: string, nullable: true }
        has_password: { type: boolean }
        unread: { type: integer }
        created_at: { type: string, format: date-time }
    CreateMailbox:
      type: object
      required: [domain_id, local_part]
      properties:
        domain_id: { type: string }
        local_part: { type: string }
        display_name: { type: string }
        password: { type: string, minLength: 12 }
        quota_mb: { type: integer }
        daily_send_limit: { type: integer, nullable: true }
        is_catch_all: { type: boolean }
        spam_level:
          type: string
          enum: [off, low, medium, high]
        reply_to: { type: string }
        signature_html: { type: string }
    UpdateMailbox:
      type: object
      properties:
        display_name: { type: string }
        is_catch_all: { type: boolean }
        signature_html: { type: string }
        reply_to: { type: string }
        spam_level:
          type: string
          nullable: true
          enum: [off, low, medium, high]
        quota_mb: { type: integer }
        daily_send_limit: { type: integer, nullable: true }
    WebhookEvent:
      type: string
      enum:
        - email.queued
        - email.sending
        - email.sent
        - email.delivered
        - email.bounced
        - email.complained
        - email.failed
        - email.opened
        - email.clicked
    Problem:
      type: object
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        errors:
          type: object
          additionalProperties: { type: string }
  responses:
    Problem:
      description: Error
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
security:
  - bearerAuth: []
