openapi: 3.1.0
info:
  title: Omnero Mail Developer API
  version: 1.0.0
  description: |
    Server-to-server API for tenant-scoped Omnero Mail integrations. API keys are
    service principals and never share dashboard or Webmail sessions.
servers:
  - url: http://localhost:3000
    description: Local development
security:
  - apiKey: []
tags:
  - name: Mailboxes
  - name: Domains
  - name: Messages
  - name: Attachments
  - name: Events
  - name: Webhooks
paths:
  /v1/mailboxes:
    get:
      operationId: listMailboxes
      tags: [Mailboxes]
      summary: List mailboxes available to the integration
      responses:
        '200':
          description: Mailbox collection
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/Mailbox' }
        default: { $ref: '#/components/responses/Error' }
  /v1/domains:
    get:
      operationId: listDomains
      tags: [Domains]
      summary: List domains and delivery health
      responses:
        '200':
          description: Domain collection
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/Domain' }
        default: { $ref: '#/components/responses/Error' }
  /v1/messages:
    get:
      operationId: listMessages
      tags: [Messages]
      summary: List messages using cursor pagination
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - in: query
          name: mailboxId
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Message page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessagePage'
        default: { $ref: '#/components/responses/Error' }
    post:
      operationId: sendMessage
      tags: [Messages]
      summary: Queue a message for delivery
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SendMessageInput' }
      responses:
        '202':
          description: Message queued
          headers:
            Idempotent-Replayed:
              schema: { type: string, enum: ['true'] }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/QueuedMessage' }
        '409': { $ref: '#/components/responses/Error' }
        default: { $ref: '#/components/responses/Error' }
  /v1/messages/{messageId}:
    parameters:
      - $ref: '#/components/parameters/MessageId'
    get:
      operationId: getMessage
      tags: [Messages]
      summary: Get a message and its MIME structure
      responses:
        '200':
          description: Message detail
          content:
            application/json:
              schema:
                type: object
                required: [message]
                properties:
                  message: { type: object, additionalProperties: true }
        default: { $ref: '#/components/responses/Error' }
    patch:
      operationId: updateMessage
      tags: [Messages]
      summary: Update flags or folder
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateMessageInput' }
      responses:
        '200':
          description: Message updated
          content:
            application/json:
              schema:
                type: object
                required: [id, updated]
                properties:
                  id: { type: string, format: uuid }
                  updated: { type: boolean, const: true }
        default: { $ref: '#/components/responses/Error' }
  /v1/attachments:
    post:
      operationId: uploadAttachment
      tags: [Attachments]
      summary: Upload an attachment for a later send
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UploadAttachmentInput' }
      responses:
        '201':
          description: Attachment created
          content:
            application/json:
              schema:
                type: object
                required: [attachment]
                properties:
                  attachment: { type: object, additionalProperties: true }
        default: { $ref: '#/components/responses/Error' }
  /v1/attachments/{attachmentId}/download:
    get:
      operationId: downloadAttachment
      tags: [Attachments]
      summary: Download a clean attachment
      parameters:
        - in: path
          name: attachmentId
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Base64 encoded attachment
          content:
            application/json:
              schema:
                type: object
                required: [filename, mediaType, contentBase64]
                properties:
                  filename: { type: string }
                  mediaType: { type: string }
                  contentBase64: { type: string, contentEncoding: base64 }
        default: { $ref: '#/components/responses/Error' }
  /v1/events:
    get:
      operationId: listEvents
      tags: [Events]
      summary: Reconcile events after webhook downtime
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/EventCursor'
      responses:
        '200':
          description: Event page
          content:
            application/json:
              schema:
                type: object
                required: [items, nextCursor]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/Event' }
                  nextCursor:
                    oneOf:
                      - { type: string, description: Opaque event pagination cursor. }
                      - { type: 'null' }
        default: { $ref: '#/components/responses/Error' }
  /v1/webhook-endpoints:
    get:
      operationId: listWebhookEndpoints
      tags: [Webhooks]
      summary: List endpoints belonging to the authenticated application
      responses:
        '200':
          description: Endpoint collection
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookEndpoint' }
        default: { $ref: '#/components/responses/Error' }
    post:
      operationId: createWebhookEndpoint
      tags: [Webhooks]
      summary: Subscribe the authenticated application to events
      description: >-
        Omnero Mail first POSTs a webhook.endpoint_verification payload to the URL.
        The endpoint must return HTTP 2xx JSON containing the exact challenge.
        Redirects are not followed and the request times out after five seconds.
        Nothing is persisted when verification fails. After success, the HMAC
        secret is returned once and signs operational deliveries.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, eventTypes]
              properties:
                url: { type: string, format: uri }
                description:
                  oneOf: [{ type: string, maxLength: 255 }, { type: 'null' }]
                eventTypes:
                  type: array
                  minItems: 1
                  maxItems: 20
                  items: { type: string }
      responses:
        '201':
          description: The secret is returned once and must be stored securely
          content:
            application/json:
              schema:
                type: object
                required: [endpoint, secret]
                properties:
                  endpoint: { $ref: '#/components/schemas/WebhookEndpoint' }
                  secret: { type: string }
        '400':
          description: URL validation or endpoint verification handshake failed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        default: { $ref: '#/components/responses/Error' }
  /v1/webhook-deliveries:
    get:
      operationId: listWebhookDeliveries
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - in: query
          name: endpointId
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Delivery collection
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { type: object, additionalProperties: true }
        default: { $ref: '#/components/responses/Error' }
  /v1/webhook-deliveries/{deliveryId}/replay:
    post:
      operationId: replayWebhookDelivery
      tags: [Webhooks]
      parameters:
        - in: path
          name: deliveryId
          required: true
          schema: { type: string, format: uuid }
      responses:
        '202':
          description: Delivery returned to the pending queue
        default: { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: mdk_test_... or mdk_live_...
      description: Tenant-scoped API key shown only once when it is created.
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema: { type: string, minLength: 8, maxLength: 255 }
      description: Reuse only for an identical request body.
    Limit:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    Cursor:
      in: query
      name: cursor
      schema: { type: string, format: date-time }
    EventCursor:
      in: query
      name: cursor
      description: Opaque event pagination cursor returned by a previous page. Legacy ISO 8601 timestamps are accepted for compatibility.
      schema: { type: string, minLength: 1, maxLength: 512 }
    MessageId:
      in: path
      name: messageId
      required: true
      schema: { type: string, format: uuid }
  responses:
    Error:
      description: Stable error envelope
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Mailbox:
      type: object
      additionalProperties: false
      required: [id, address, displayName]
      properties:
        id: { type: string, format: uuid }
        address: { type: string, format: email }
        displayName: { type: string }
    Domain:
      type: object
      additionalProperties: false
      required: [id, domain, verified, health, mailboxCount]
      properties:
        id: { type: string, format: uuid }
        domain: { type: string }
        verified: { type: boolean }
        health: { type: string }
        mailboxCount: { type: integer, minimum: 0 }
    MessageSummary:
      type: object
      additionalProperties: true
      required: [id, createdAt]
      properties:
        id: { type: string, format: uuid }
        createdAt: { type: string, format: date-time }
    MessagePage:
      type: object
      required: [items, nextCursor]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/MessageSummary' }
        nextCursor:
          oneOf:
            - { type: string, format: date-time }
            - { type: 'null' }
    SendMessageInput:
      type: object
      additionalProperties: false
      required: [fromMailboxId, to]
      anyOf:
        - required: [text]
        - required: [html]
      properties:
        fromMailboxId: { type: string, format: uuid }
        to:
          type: array
          minItems: 1
          maxItems: 50
          items: { type: string, format: email }
        cc:
          type: array
          maxItems: 50
          default: []
          items: { type: string, format: email }
        bcc:
          type: array
          maxItems: 50
          default: []
          items: { type: string, format: email }
        subject: { type: string, maxLength: 998, default: '' }
        text:
          oneOf: [{ type: string, maxLength: 1000000 }, { type: 'null' }]
        html:
          oneOf: [{ type: string, maxLength: 2000000 }, { type: 'null' }]
        attachmentIds:
          type: array
          maxItems: 20
          default: []
          items: { type: string, format: uuid }
    QueuedMessage:
      type: object
      additionalProperties: false
      required: [id, object, status]
      properties:
        id: { type: string, format: uuid }
        object: { type: string, const: message }
        status: { type: string, const: queued }
    UpdateMessageInput:
      type: object
      additionalProperties: false
      required: [mailboxId]
      properties:
        mailboxId: { type: string, format: uuid }
        isRead: { type: boolean }
        isStarred: { type: boolean }
        folder: { type: string, enum: [inbox, sent, archive, spam, trash] }
    UploadAttachmentInput:
      type: object
      additionalProperties: false
      required: [mailboxId, filename, mediaType, contentBase64]
      properties:
        mailboxId: { type: string, format: uuid }
        filename: { type: string, minLength: 1, maxLength: 255 }
        mediaType: { type: string, minLength: 1, maxLength: 120 }
        contentBase64: { type: string, contentEncoding: base64 }
    Event:
      type: object
      additionalProperties: false
      required: [id, type, resourceType, resourceId, data, occurredAt]
      properties:
        id: { type: string, format: uuid }
        type: { type: string }
        resourceType: { type: string }
        resourceId: { type: string, format: uuid }
        data: { type: object, additionalProperties: true }
        occurredAt: { type: string, format: date-time }
    WebhookEndpoint:
      type: object
      additionalProperties: false
      required: [id, appId, url, eventTypes, enabled, consecutiveFailures, disabledAt, createdAt]
      properties:
        id: { type: string, format: uuid }
        appId: { type: string, format: uuid }
        url: { type: string, format: uri }
        description:
          oneOf: [{ type: string }, { type: 'null' }]
        eventTypes:
          type: array
          items: { type: string }
        enabled: { type: boolean }
        consecutiveFailures: { type: integer, minimum: 0 }
        disabledAt:
          oneOf: [{ type: string, format: date-time }, { type: 'null' }]
        createdAt: { type: string, format: date-time }
    Error:
      type: object
      additionalProperties: false
      required: [code, message]
      properties:
        error: { type: string }
        code: { type: string }
        message: { type: string }
        requestId: { type: string }
