openapi: 3.1.0
info:
  title: SRS Postline API
  version: 1.0.0
  description: |
    REST API for transactional email sending and management.
    A 202 response means queued, not delivered. Live keys are available immediately;
    before a customer From-domain is verified, only the shared sandbox sender may
    send and only to project-member addresses. Quotas, suppression, recipient
    throttles, and abuse controls apply to every plan.
  contact:
    name: SRS Postline support
    url: https://srs-postline.com/en/docs/
  license:
    name: Proprietary
externalDocs:
  description: Integration guide with examples for 15 programming languages
  url: https://srs-postline.com/guides/integration-guide.md
servers:
  - url: https://api.srs-postline.com
    description: Production
  - url: http://localhost:8080
    description: Local development
security:
  - bearerAuth: []
paths:
  /health:
    get:
      summary: Live API, database, and queue readiness
      operationId: getHealth
      security: []
      responses:
        '200': { description: All checked components are operational }
        '503': { description: At least one checked component is unavailable }
  /v1/auth/request-code:
    post:
      summary: Request a passwordless login code
      operationId: requestLoginCode
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestLoginCode'
      responses:
        '202':
          description: The code was accepted for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginCodeAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/verify-code:
    post:
      summary: Verify a passwordless login code
      operationId: verifyLoginCode
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyLoginCode'
      responses:
        '200':
          description: Authenticated session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyLoginCodeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/login-password:
    post:
      summary: Sign in with a configured password
      operationId: loginWithPassword
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordLogin'
      responses:
        '200':
          description: Authenticated session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyLoginCodeResponse'
        '401':
          description: Invalid credentials, invalid TOTP, or TOTP required
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/auth/session:
    get:
      summary: Get the current dashboard session
      operationId: getUserSession
      security:
        - sessionAuth: []
      responses:
        '200':
          description: Current session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/auth/logout:
    post:
      summary: Revoke the current dashboard session
      operationId: logoutUserSession
      security:
        - sessionAuth: []
      responses:
        '204':
          description: Session revoked
  /v1/emails:
    post:
      summary: Send email
      operationId: sendEmail
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: Stable identifier for one logical send. Strongly recommended for every request; retries with the same value return the existing message.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
      responses:
        '202':
          description: Email queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/Suppressed'
        '429':
          $ref: '#/components/responses/RateLimited'
    get:
      summary: List emails (requires emails:read)
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: Email list
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MessageListItem'
  /v1/emails/{id}:
    get:
      summary: Get email by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
  /v1/emails/{id}/schedule:
    delete:
      summary: Cancel an email that has not reached its scheduled time
      operationId: cancelScheduledEmail
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Scheduled send cancelled }
        '404': { description: Message is not scheduled or does not exist }
  /v1/emails/batch:
    post:
      summary: Queue up to 100 transactional emails
      operationId: sendEmailBatch
      parameters:
        - name: Idempotency-Key
          in: header
          description: Base key; item indexes are appended when an item has no idempotency_key.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [emails]
              properties:
                emails:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    allOf:
                      - $ref: '#/components/schemas/SendEmailRequest'
                      - type: object
                        properties:
                          idempotency_key: { type: string }
      responses:
        '202': { description: Every item was accepted }
        '207': { description: Some items failed; inspect each result }
  /v1/analytics:
    get:
      summary: Aggregated delivery and provider analytics
      operationId: getAnalytics
      parameters:
        - name: days
          in: query
          schema: { type: integer, minimum: 1, maximum: 90, default: 30 }
      responses:
        '200':
          description: Analytics report
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AnalyticsReport' }
  /v1/templates:
    get:
      summary: List email templates
      operationId: listTemplates
      responses:
        '200': { description: Templates }
    post:
      summary: Create a draft email template
      operationId: createTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmailTemplateRequest' }
      responses:
        '201': { description: Draft created }
  /v1/templates/{id}:
    put:
      summary: Save a new immutable draft version
      operationId: updateTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmailTemplateRequest' }
      responses:
        '200': { description: New draft version }
    delete:
      summary: Delete a template and all versions
      operationId: deleteTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Deleted }
  /v1/templates/{id}/versions:
    get:
      summary: List immutable template versions
      operationId: listTemplateVersions
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Version history }
  /v1/templates/{id}/publish:
    post:
      summary: Publish the current draft version
      operationId: publishTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Published template }
  /v1/templates/{id}/rollback/{version}:
    post:
      summary: Point sending back to an immutable prior version
      operationId: rollbackTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: version
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        '200': { description: Published version changed }
  /v1/domains:
    post:
      summary: Create sending domain
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDomainRequest'
      responses:
        '201':
          description: Domain created
    get:
      summary: List domains
      responses:
        '200':
          description: Domain list
  /v1/domains/{id}/verify:
    post:
      summary: Trigger domain verification
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Verification triggered
  /v1/suppressions/{email}:
    get:
      summary: Check suppression
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Suppression found
        '404':
          description: Not suppressed
    delete:
      summary: Remove suppression
      responses:
        '204':
          description: Removed
  /v1/suppressions:
    post:
      summary: Add suppression
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuppressionRequest'
      responses:
        '201':
          description: Created
  /v1/webhooks:
    post:
      summary: Create webhook endpoint
      responses:
        '201':
          description: Created
    get:
      summary: List webhook endpoints
      responses:
        '200':
          description: List
  /v1/webhooks/{id}:
    delete:
      summary: Delete webhook endpoint
      responses:
        '204':
          description: Deleted
  /v1/webhooks/{id}/deliveries:
    get:
      summary: Inspect webhook attempts including timing and safe headers
      operationId: listWebhookDeliveries
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        '200': { description: Delivery attempts }
  /v1/webhooks/{id}/deliveries/{deliveryId}:
    get:
      summary: Inspect one webhook attempt
      operationId: getWebhookDelivery
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: deliveryId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Delivery attempt }
  /v1/webhooks/{id}/deliveries/{deliveryId}/replay:
    post:
      summary: Replay the original signed webhook payload
      operationId: replayWebhookDelivery
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: deliveryId
          in: path
          required: true
          schema: { type: string }
      responses:
        '202': { description: Replay queued }
  /v1/status:
    get:
      summary: Public component status and persisted incident history
      security: []
      responses:
        '200': { description: Current status, components, and incidents }
  /v1/status/rss.xml:
    get:
      summary: Read the public incident RSS feed
      security: []
      responses: { '200': { description: RSS 2.0 incident feed } }
  /v1/status/subscriptions:
    post:
      summary: Subscribe to incident notifications
      description: Email requires link verification; webhook requires an HTTPS challenge response.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [channel, destination]
              properties:
                channel: { type: string, enum: [email, webhook] }
                destination: { type: string }
      responses:
        '201': { description: Pending or verified subscription }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/status/subscriptions/verify/{token}:
    get:
      summary: Verify an email incident subscription
      security: []
      parameters: [{ name: token, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Subscription verified } }
  /v1/status/subscriptions/unsubscribe/{token}:
    get:
      summary: Unsubscribe from incident notifications
      security: []
      parameters: [{ name: token, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Subscription removed } }
  /v1/tracking:
    get:
      summary: Get open and click tracking settings
      responses: { '200': { description: Tracking settings } }
    put:
      summary: Configure privacy-aware open and click tracking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [opens_enabled, clicks_enabled, retention_days]
              properties:
                opens_enabled: { type: boolean }
                clicks_enabled: { type: boolean }
                retention_days: { type: integer, minimum: 1, maximum: 365 }
                custom_domain: { type: string }
      responses: { '200': { description: Updated tracking settings } }
  /v1/inbound/routes:
    get:
      summary: List inbound routes
      responses: { '200': { description: Routes } }
    post:
      summary: Create a route on a verified domain; omit local_part for catch-all
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain_id]
              properties:
                domain_id: { type: string }
                local_part: { type: string }
      responses: { '201': { description: Route created } }
  /v1/inbound/routes/{id}:
    delete:
      summary: Disable and remove an inbound route
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '204': { description: Route removed } }
  /v1/inbound/emails:
    get:
      summary: List received messages
      responses: { '200': { description: Received messages } }
  /v1/inbound/emails/{id}:
    get:
      summary: Get a received message
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Received message } }
  /v1/inbound/emails/{id}/reply:
    post:
      summary: Reply while preserving In-Reply-To and References threading
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string, maxLength: 200 } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from]
              properties: { from: { type: string }, html: { type: string }, text: { type: string } }
      responses: { '202': { description: Reply queued } }
  /v1/inbound/attachments/{id}/download:
    get:
      summary: Download a clean inbound attachment
      description: Available only after a successful antivirus scan.
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: Attachment bytes }
        '404': { description: Attachment unsafe, unavailable, or not found }
  /v1/inbound/threads/{id}:
    get:
      summary: Get a complete inbound thread
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Ordered messages } }
  /v1/audiences:
    get:
      summary: List consent-based audiences
      responses: { '200': { description: Audiences } }
    post:
      summary: Create an audience
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string }, description: { type: string } } } } }
      responses: { '201': { description: Audience created } }
  /v1/audiences/{id}/contacts:
    post:
      summary: Add or resubscribe a contact with recorded consent
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, consent_source, consent_at]
              properties:
                email: { type: string, format: email }
                first_name: { type: string }
                last_name: { type: string }
                metadata: { type: object, additionalProperties: { type: string } }
                consent_source: { type: string }
                consent_at: { type: string, format: date-time }
      responses: { '201': { description: Contact stored } }
  /v1/broadcasts:
    get:
      summary: List broadcasts
      responses: { '200': { description: Broadcasts } }
    post:
      summary: Create a draft broadcast
      responses: { '201': { description: Draft created } }
  /v1/broadcasts/{id}/schedule:
    post:
      summary: Schedule a consent-filtered broadcast on the isolated marketing queue
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '202': { description: Broadcast scheduled } }
  /v1/broadcasts/{id}/cancel:
    post:
      summary: Cancel a draft or scheduled broadcast
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '204': { description: Broadcast cancelled } }
  /v1/validation:
    post:
      summary: Validate syntax, MX, disposable domains, and role accounts
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string, format: email } } } } }
      responses: { '200': { description: Cached or fresh validation result } }
  /v1/reputation:
    get:
      summary: Reputation, dedicated IP reviews, assignments, and warmup plans
      responses: { '200': { description: Reputation center } }
  /v1/dedicated-ips/requests:
    post:
      summary: Request operator-reviewed dedicated IP provisioning
      responses: { '202': { description: Request accepted for review } }
  /v1/unsubscribe/{token}:
    get:
      summary: Unsubscribe a consent contact
      security: []
      parameters: [{ name: token, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Contact unsubscribed } }
  /v1/dashboard/emails:
    get:
      summary: Search and paginate the dashboard message history
      security: [{ sessionAuth: [] }]
      parameters:
        - { name: X-Postline-Project-ID, in: header, schema: { type: string } }
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: page_size, in: query, schema: { type: integer, minimum: 10, maximum: 100, default: 20 } }
        - { name: search, in: query, description: Matches message ID, sender, recipient, or subject., schema: { type: string, maxLength: 200 } }
        - { name: status, in: query, schema: { type: string } }
        - { name: sort, in: query, schema: { type: string, enum: [created_at, recipient, subject, status, from], default: created_at } }
        - { name: order, in: query, schema: { type: string, enum: [asc, desc], default: desc } }
      responses:
        '200':
          description: Paginated message history
          content: { application/json: { schema: { $ref: '#/components/schemas/DashboardMessagePage' } } }
  /v1/dashboard/emails/{id}:
    get:
      summary: Read stored message metadata and body
      description: Returns the text and HTML originally submitted for the selected project message. Attachment metadata is returned without attachment content.
      security: [{ sessionAuth: [] }]
      parameters:
        - { name: X-Postline-Project-ID, in: header, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Message details
          content: { application/json: { schema: { $ref: '#/components/schemas/DashboardMessageDetail' } } }
        '404': { description: Message not found in the selected project }
  /v1/dashboard/send-approvals:
    get:
      summary: List agent sends awaiting human approval
      security: [{ sessionAuth: [] }]
      responses: { '200': { description: Approval requests } }
  /v1/dashboard/send-approvals/{id}/approve:
    post:
      summary: Approve and enqueue an agent send
      security: [{ sessionAuth: [] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '202': { description: Send approved and accepted } }
  /v1/dashboard/send-approvals/{id}/reject:
    post:
      summary: Reject an agent send
      security: [{ sessionAuth: [] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { note: { type: string, maxLength: 500 } } }
      responses: { '204': { description: Send rejected } }
  /v1/dashboard/organization/members:
    get:
      summary: List organization members and RBAC roles
      security: [{ sessionAuth: [] }]
      responses: { '200': { description: Organization members } }
    post:
      summary: Add an organization member
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, role]
              properties:
                email: { type: string, format: email }
                role: { type: string, enum: [owner, admin, developer, analyst, billing] }
      responses: { '201': { description: Member added } }
  /v1/dashboard/organization/members/{id}:
    put:
      summary: Change an organization member role
      security: [{ sessionAuth: [] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [role], properties: { role: { type: string, enum: [owner, admin, developer, analyst, billing] } } }
      responses: { '204': { description: Role changed } }
    delete:
      summary: Remove a member and revoke their sessions
      security: [{ sessionAuth: [] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '204': { description: Member removed } }
  /v1/dashboard/audit-logs:
    get:
      summary: List organization audit records
      security: [{ sessionAuth: [] }]
      parameters: [{ name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }]
      responses: { '200': { description: Audit records } }
  /v1/dashboard/reputation/connections:
    get:
      summary: List reputation-provider connections
      security: [{ sessionAuth: [] }]
      responses: { '200': { description: Connections } }
    post:
      summary: Verify and store an encrypted provider credential
      security: [{ sessionAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, credential]
              properties:
                provider: { type: string, enum: [google_postmaster, microsoft_snds] }
                credential: { type: string, writeOnly: true }
      responses: { '201': { description: Provider connected } }
  /v1/dashboard/reputation/connections/{id}/sync:
    post:
      summary: Synchronize a reputation-provider connection
      security: [{ sessionAuth: [] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: Provider synchronized } }
  /v1/dashboard/reputation/connections/{id}:
    delete:
      summary: Revoke a connection and erase its credential
      security: [{ sessionAuth: [] }]
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { '204': { description: Provider disconnected } }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key prefixed with srs_live_ or srs_test_
    sessionAuth:
      type: http
      scheme: bearer
      description: Dashboard session token prefixed with pls_
  schemas:
    RequestLoginCode:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        locale:
          type: string
          enum: [en, ru, es, fr, pt, zh, hi, bn, id, et, ar]
    LoginCodeAccepted:
      type: object
      required: [ok, expires_in]
      properties:
        ok:
          type: boolean
          const: true
        expires_in:
          type: integer
          const: 600
    VerifyLoginCode:
      type: object
      required: [email, code]
      properties:
        email:
          type: string
          format: email
        code:
          type: string
          pattern: '^\d{6}$'
          example: '428173'
        totp_code:
          type: string
          pattern: '^\d{6}$'
          description: Required when authenticator-based two-factor authentication is enabled.
    PasswordLogin:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 12
          maxLength: 128
        totp_code:
          type: string
          pattern: '^\d{6}$'
          description: Required when authenticator-based two-factor authentication is enabled.
    VerifyLoginCodeResponse:
      type: object
      required: [token, session]
      properties:
        token:
          type: string
          description: Return this token as a Bearer credential. The dashboard proxy stores it in an HttpOnly cookie.
        session:
          $ref: '#/components/schemas/UserSession'
    UserSession:
      type: object
      required: [id, user_id, email, organization_id, project_id, project_status, auth_method, created_at, last_seen_at, expires_at]
      properties:
        id:
          type: string
        user_id:
          type: string
        email:
          type: string
          format: email
        organization_id:
          type: string
        project_id:
          type: string
        project_status:
          type: string
          enum: [sandbox, active, suspended]
        ip_address:
          type: string
        user_agent:
          type: string
        auth_method:
          type: string
          enum: [email_code, password]
        created_at:
          type: string
          format: date-time
        last_seen_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
    SendEmailRequest:
      type: object
      required: [from, to]
      oneOf:
        - required: [subject, html]
        - required: [subject, text]
        - required: [template, variables]
      description: Attachments are not supported. Only threading and RFC 8058 unsubscribe headers are accepted.
      properties:
        from:
          type: string
          example: "SRS Postline <info@example.com>"
        to:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            format: email
        cc:
          type: array
          maxItems: 100
          items:
            type: string
            format: email
        bcc:
          type: array
          maxItems: 100
          items:
            type: string
            format: email
        reply_to:
          type: array
          items:
            type: string
            format: email
        subject:
          type: string
        html:
          type: string
        text:
          type: string
        tags:
          type: object
          additionalProperties:
            type: string
        template:
          type: string
          description: Published template ID or slug. Do not combine with subject/html/text.
        variables:
          type: object
          additionalProperties: { type: string }
          description: Exact variables declared by the published version. HTML values are escaped.
        send_at:
          type: string
          format: date-time
          description: Schedule up to 30 days ahead. Omit for immediate delivery.
        track_opens:
          type: boolean
          description: Per-message override for project open tracking.
        track_clicks:
          type: boolean
          description: Per-message override for project click tracking.
    SendEmailResponse:
      type: object
      properties:
        id:
          type: string
          example: msg_01JPOSTLINEEXAMPLE
        status:
          type: string
          example: queued
        created_at:
          type: string
          format: date-time
    MessageListItem:
      type: object
      required: [id, from, to, subject, status, created_at]
      properties:
        id:
          type: string
        from:
          type: string
        to:
          type: array
          items:
            type: string
        subject:
          type: string
        status:
          type: string
        created_at:
          type: string
          format: date-time
    DashboardMessagePage:
      type: object
      required: [data, page, page_size, total, total_pages, sort, order]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/MessageListItem' }
        page: { type: integer, minimum: 1 }
        page_size: { type: integer, minimum: 10, maximum: 100 }
        total: { type: integer, minimum: 0 }
        total_pages: { type: integer, minimum: 0 }
        sort: { type: string }
        order: { type: string, enum: [asc, desc] }
    DashboardMessageDetail:
      type: object
      required: [id, from, recipients, reply_to, subject, headers, tags, attachments, status, channel, created_at, updated_at]
      properties:
        id: { type: string }
        from: { type: string }
        recipients:
          type: array
          items:
            type: object
            required: [email, type, status]
            properties:
              email: { type: string, format: email }
              type: { type: string, enum: [to, cc, bcc] }
              status: { type: string }
              mta_queue_id: { type: string }
        reply_to: { type: array, items: { type: string } }
        subject: { type: string }
        html: { type: string }
        text: { type: string }
        headers: { type: object, additionalProperties: { type: string } }
        tags: { type: object, additionalProperties: { type: string } }
        attachments:
          type: array
          items:
            type: object
            required: [id, filename, content_type, size_bytes]
            properties:
              id: { type: string }
              filename: { type: string }
              content_type: { type: string }
              size_bytes: { type: integer, format: int64 }
        status: { type: string }
        channel: { type: string }
        scheduled_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    EmailTemplateRequest:
      type: object
      required: [name, subject, variables]
      anyOf:
        - required: [html_body]
        - required: [text_body]
      properties:
        slug:
          type: string
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
        name: { type: string, maxLength: 80 }
        subject: { type: string, maxLength: 300 }
        html_body: { type: string }
        text_body: { type: string }
        variables:
          type: array
          maxItems: 50
          items: { type: string, maxLength: 64 }
    AnalyticsReport:
      type: object
      required: [days, sent, delivered, bounced, deferred, complained, delivery_rate, bounce_rate, complaint_rate, daily, providers]
      properties:
        days: { type: integer }
        sent: { type: integer }
        delivered: { type: integer }
        bounced: { type: integer }
        deferred: { type: integer }
        complained: { type: integer }
        unique_opens: { type: integer }
        unique_clicks: { type: integer }
        open_rate: { type: number, minimum: 0 }
        click_rate: { type: number, minimum: 0 }
        delivery_rate: { type: number, minimum: 0, maximum: 1 }
        bounce_rate: { type: number, minimum: 0, maximum: 1 }
        complaint_rate: { type: number, minimum: 0, maximum: 1 }
        median_delivery_latency_ms: { type: integer }
        daily:
          type: array
          items:
            type: object
            properties:
              day: { type: string, format: date }
              accepted: { type: integer }
              queued: { type: integer }
              delivered: { type: integer }
              bounced: { type: integer }
              deferred: { type: integer }
              complained: { type: integer }
        providers:
          type: array
          items:
            type: object
            properties:
              provider: { type: string }
              total: { type: integer }
              queued: { type: integer }
              delivered: { type: integer }
              bounced: { type: integer }
              deferred: { type: integer }
              complained: { type: integer }
              delivery_rate: { type: number }
              median_delivery_latency_ms: { type: integer }
    CreateDomainRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        region:
          type: string
          default: eu
    SuppressionRequest:
      type: object
      required: [email, reason]
      properties:
        email:
          type: string
        reason:
          type: string
          enum: [hard_bounce, spam_complaint, unsubscribe, manual_block, repeated_soft_bounce, abuse_decision, invalid_address]
  responses:
    BadRequest:
      description: Invalid payload
    Unauthorized:
      description: Invalid API key
    Forbidden:
      description: Domain not verified or missing scope
    Suppressed:
      description: Recipient on suppression list
    RateLimited:
      description: Rate limit exceeded
