openapi: 3.1.0
info:
  title: PostMX Public API
  version: "2026-03-28"
  description: |
    Public API V1 for dashboard-issued or CLI-issued PostMX API keys.
    All responses include x-request-id and x-postmx-api-version.
    /v1 responses also include Cache-Control: no-store.
    /v1 also has a lightweight per-IP abuse shield before API key lookup and may return 429 before account auth completes.
servers:
  - url: https://api.postmx.co
security:
  - bearerAuth: []
tags:
  - name: Auth
  - name: Inboxes
  - name: Messages
  - name: Webhooks
paths:
  /health:
    get:
      summary: Health check
      security: []
      responses:
        "200":
          description: Worker is reachable
  /v1/auth/cli/email/start:
    post:
      tags: [Auth]
      summary: Start CLI email sign-in
      security: []
      parameters:
        - $ref: "#/components/parameters/XPostmxClient"
        - $ref: "#/components/parameters/XPostmxClientVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CliEmailStartRequest"
      responses:
        "200":
          description: CLI auth challenge created
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - $ref: "#/components/schemas/CliEmailStartResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/cli/email/verify:
    get:
      tags: [Auth]
      summary: Complete CLI magic-link sign-in
      security: []
      parameters:
        - name: auth_request
          in: query
          required: true
          schema:
            type: string
        - name: challenge
          in: query
          required: true
          schema:
            type: string
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirects to the dashboard CLI completion page with a dashboard session token and one-time CLI exchange code in the URL fragment.
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
    post:
      tags: [Auth]
      summary: Complete CLI OTP sign-in
      security: []
      parameters:
        - $ref: "#/components/parameters/XPostmxClient"
        - $ref: "#/components/parameters/XPostmxClientVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CliEmailVerifyRequest"
      responses:
        "200":
          description: CLI auth completed and public API key issued
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - $ref: "#/components/schemas/CliAuthCompleteResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/cli/exchange:
    post:
      tags: [Auth]
      summary: Exchange one-time CLI approval code for an API key
      security: []
      parameters:
        - $ref: "#/components/parameters/XPostmxClient"
        - $ref: "#/components/parameters/XPostmxClientVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CliExchangeRequest"
      responses:
        "200":
          description: CLI auth completed and public API key issued
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - $ref: "#/components/schemas/CliAuthCompleteResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
  /v1/inboxes:
    get:
      tags: [Inboxes]
      summary: List active inboxes
      description: Returns a paginated list of the authenticated account's active inboxes in reverse creation order.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Inboxes listed
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      wildcard_address:
                        allOf:
                          - $ref: "#/components/schemas/WildcardAddress"
                        nullable: true
                      inboxes:
                        type: array
                        items:
                          $ref: "#/components/schemas/Inbox"
                      page_info:
                        $ref: "#/components/schemas/PageInfo"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
    post:
      tags: [Inboxes]
      summary: Create inbox
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInboxRequest"
      responses:
        "201":
          description: Inbox created
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      inbox:
                        $ref: "#/components/schemas/Inbox"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
  /v1/inboxes/{inbox_id}/messages:
    get:
      tags: [Messages]
      summary: List inbox messages
      description: Returns a paginated message summary feed. Full message bodies are available from GET /v1/messages/{message_id}.
      parameters:
        - name: inbox_id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Messages listed
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      messages:
                        type: array
                        items:
                          $ref: "#/components/schemas/MessageSummary"
                      page_info:
                        $ref: "#/components/schemas/PageInfo"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
  /v1/messages:
    get:
      tags: [Messages]
      summary: List messages by recipient address
      description: Returns a paginated message summary feed for an exact recipient email address within the authenticated account. Full message bodies are available from GET /v1/messages/{message_id}.
      parameters:
        - name: recipient_email
          in: query
          required: true
          description: Exact recipient email address to match against stored message `to_email` values. Matching is case-insensitive after normalization.
          schema:
            type: string
            format: email
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Messages listed
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      messages:
                        type: array
                        items:
                          $ref: "#/components/schemas/MessageSummary"
                      page_info:
                        $ref: "#/components/schemas/PageInfo"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
  /v1/messages/{message_id}:
    get:
      tags: [Messages]
      summary: Get message detail
      description: Returns a single message. By default the full message detail is included, and `content_mode` can be used to return only the OTP, links, or plain-text body payload. All detail variants also include the additive `analysis` object for asynchronous structured enrichment state and results.
      parameters:
        - name: message_id
          in: path
          required: true
          schema:
            type: string
        - name: content_mode
          in: query
          required: false
          description: Selects a reduced payload. `full` returns the complete message detail, while `otp`, `links`, and `text_only` return only that focused payload plus message metadata and the additive `analysis` object.
          schema:
            type: string
            default: full
            enum: [full, otp, links, text_only]
      responses:
        "200":
          description: Message detail
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      message:
                        oneOf:
                          - $ref: "#/components/schemas/MessageDetail"
                          - $ref: "#/components/schemas/MessageOtpDetail"
                          - $ref: "#/components/schemas/MessageLinksDetail"
                          - $ref: "#/components/schemas/MessageTextOnlyDetail"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
  /v1/webhooks:
    post:
      tags: [Webhooks]
      summary: Create webhook
      description: Registers a webhook target for `email.received` and `email.enriched` events. `email.received` is emitted immediately after message storage; `email.enriched` is emitted later only when background message analysis completes successfully. Use a final public HTTPS endpoint on a dedicated non-root path that returns a direct 2xx response and does not redirect to login or dashboard flows.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookRequest"
      responses:
        "201":
          description: Webhook created
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-PostMX-Api-Version:
              $ref: "#/components/headers/XPostmxApiVersion"
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      webhook:
                        $ref: "#/components/schemas/Webhook"
                      signing_secret:
                        type: string
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: pmx_live API key
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
    XPostmxClient:
      name: X-PostMX-Client
      in: header
      required: true
      schema:
        type: string
        const: cli
    XPostmxClientVersion:
      name: X-PostMX-Client-Version
      in: header
      required: true
      schema:
        type: string
  responses:
    ErrorResponse:
      description: Error envelope
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
        X-PostMX-Api-Version:
          $ref: "#/components/headers/XPostmxApiVersion"
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimitedResponse:
      description: Rate limited error envelope from either the per-IP abuse shield or authenticated account budgets
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
        X-PostMX-Api-Version:
          $ref: "#/components/headers/XPostmxApiVersion"
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
  headers:
    XRequestId:
      description: Request identifier for tracing and support.
      schema:
        type: string
    XPostmxApiVersion:
      description: Lightweight release stamp for the deployed public API worker.
      schema:
        type: string
        const: "2026-03-28"
    CacheControlNoStore:
      description: /v1 responses are not cacheable.
      schema:
        type: string
        const: no-store
    RetryAfter:
      description: Number of seconds to wait before retrying a rate-limited request.
      schema:
        type: integer
  schemas:
    SuccessEnvelope:
      type: object
      required: [success, request_id]
      properties:
        success:
          type: boolean
          const: true
        request_id:
          type: string
    ErrorEnvelope:
      type: object
      required: [success, request_id, error]
      properties:
        success:
          type: boolean
          const: false
        request_id:
          type: string
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            retry_after_seconds:
              type: integer
        next_action:
          $ref: "#/components/schemas/NextAction"
    NextAction:
      type: object
      required: [type, url]
      properties:
        type:
          type: string
          enum: [open_dashboard]
        url:
          type: string
          format: uri
    CliEmailStartRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        scopes:
          type: array
          items:
            type: string
            enum: [inboxes:write, messages:read, webhooks:write]
        label:
          type: string
          minLength: 1
          maxLength: 100
        expires_at:
          type: string
          format: date-time
        localhost_callback_url:
          type: string
          description: Optional loopback-only HTTP callback URL such as `http://127.0.0.1:8787/callback`.
        callback_state:
          type: string
          maxLength: 512
        next_dashboard_path:
          type: string
    CliEmailStartResponse:
      type: object
      required: [auth_request_id, challenge_id, expires_at, email, delivery]
      properties:
        auth_request_id:
          type: string
        challenge_id:
          type: string
        expires_at:
          type: string
        email:
          type: string
        delivery:
          type: object
          required: [channel, methods]
          properties:
            channel:
              type: string
              const: email
            methods:
              type: array
              items:
                type: string
                enum: [magic_link, otp]
    CliEmailVerifyRequest:
      type: object
      required: [auth_request_id, challenge_id, code]
      properties:
        auth_request_id:
          type: string
        challenge_id:
          type: string
        code:
          type: string
    CliExchangeRequest:
      type: object
      required: [auth_request_id, code]
      properties:
        auth_request_id:
          type: string
        code:
          type: string
    CliAuthCompleteResponse:
      type: object
      required:
        - api_key
        - key
        - user
        - account
        - billing
        - onboarding
      properties:
        api_key:
          type: string
        key:
          $ref: "#/components/schemas/IssuedApiKey"
        user:
          $ref: "#/components/schemas/CliAuthUser"
        account:
          $ref: "#/components/schemas/CliAuthAccount"
        billing:
          $ref: "#/components/schemas/CliAuthBilling"
        onboarding:
          $ref: "#/components/schemas/CliAuthOnboarding"
        dashboard_url:
          type: string
          nullable: true
        dashboard_billing_url:
          type: string
          nullable: true
    IssuedApiKey:
      type: object
      required: [id, label, key_prefix, scopes, created_at]
      properties:
        id:
          type: string
        label:
          type: string
        key_prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
            enum: [inboxes:write, messages:read, webhooks:write]
        last_used_at:
          type: string
          nullable: true
        expires_at:
          type: string
          nullable: true
        revoked_at:
          type: string
          nullable: true
        created_at:
          type: string
        created_by_user_id:
          type: string
          nullable: true
    CliAuthUser:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
        email:
          type: string
        display_name:
          type: string
          nullable: true
        avatar_url:
          type: string
          nullable: true
        github_login:
          type: string
          nullable: true
    CliAuthAccount:
      type: object
      required: [id, name, slug, role, permissions]
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        role:
          type: string
        permissions:
          type: object
          required: [manage_api_keys, manage_inboxes, manage_webhooks]
          properties:
            manage_api_keys:
              type: boolean
            manage_inboxes:
              type: boolean
            manage_webhooks:
              type: boolean
    CliAuthBilling:
      type: object
      required: [tier, status, entitlements, usage]
      properties:
        tier:
          type: string
        status:
          type: string
        current_period_end:
          type: string
          nullable: true
        grace_until:
          type: string
          nullable: true
        entitlements:
          type: object
          required:
            - active_inboxes
            - monthly_received_messages
            - api_requests
            - message_retention_days
            - webhook_mode
            - support_level
            - managed_subdomain_namespace
            - wildcard_namespace
          properties:
            active_inboxes:
              type: integer
            monthly_received_messages:
              type: integer
            api_requests:
              type: object
              required: [minute, day, week, month]
              properties:
                minute:
                  type: integer
                day:
                  type: integer
                week:
                  type: integer
                month:
                  type: integer
            message_retention_days:
              type: integer
            webhook_mode:
              type: string
            support_level:
              type: string
            managed_subdomain_namespace:
              type: boolean
            wildcard_namespace:
              type: boolean
        usage:
          type: object
          required: [active_inboxes, monthly_received_messages]
          properties:
            active_inboxes:
              type: object
              required: [used, limit]
              properties:
                used:
                  type: integer
                limit:
                  type: integer
            monthly_received_messages:
              type: object
              required: [used, limit, usage_month]
              properties:
                used:
                  type: integer
                limit:
                  type: integer
                usage_month:
                  type: string
    CliAuthOnboarding:
      type: object
      required: [required, completed, steps]
      properties:
        required:
          type: boolean
        completed:
          type: boolean
        steps:
          type: object
          required: [create_inbox, configure_webhook, claim_managed_subdomain]
          properties:
            create_inbox:
              type: boolean
            configure_webhook:
              type: boolean
            claim_managed_subdomain:
              type: boolean
    CreateInboxRequest:
      type: object
      required: [label, lifecycle_mode]
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 100
        lifecycle_mode:
          type: string
          enum: [temporary, persistent]
        ttl_minutes:
          type: integer
          minimum: 10
          maximum: 60
        message_analysis:
          $ref: "#/components/schemas/InboxMessageAnalysis"
    Inbox:
      type: object
      required: [id, label, email_address, lifecycle_mode, status, created_at, message_analysis]
      properties:
        id:
          type: string
        label:
          type: string
        email_address:
          type: string
        lifecycle_mode:
          type: string
          enum: [temporary, persistent]
        ttl_minutes:
          type: integer
          nullable: true
        expires_at:
          type: string
          nullable: true
        last_message_received_at:
          type: string
          nullable: true
        status:
          type: string
        created_at:
          type: string
        message_analysis:
          $ref: "#/components/schemas/InboxMessageAnalysis"
    InboxMessageAnalysis:
      type: object
      required: [mode, recipients]
      properties:
        mode:
          type: string
          enum: [all, disabled, allowlist, blocklist]
          description: Controls whether background message analysis runs for this inbox. Wildcard inboxes default to `disabled`.
        recipients:
          type: array
          description: Exact recipient email addresses used by `allowlist` and `blocklist` modes.
          items:
            type: string
            format: email
    WildcardAddress:
      type: object
      required: [email_address, inbox_id]
      properties:
        email_address:
          type: string
          description: Managed namespace wildcard pattern for the account, for example `*@apple.postmx.co`.
        inbox_id:
          type: string
    MessageSummary:
      type: object
      required:
        - id
        - inbox_id
        - inbox_email_address
        - inbox_label
        - from_email
        - to_email
        - received_at
        - has_text_body
        - has_html_body
      properties:
        id:
          type: string
        inbox_id:
          type: string
        inbox_email_address:
          type: string
        inbox_label:
          type: string
        from_email:
          type: string
        to_email:
          type: string
        subject:
          type: string
          nullable: true
        preview_text:
          type: string
          nullable: true
        received_at:
          type: string
        has_text_body:
          type: boolean
        has_html_body:
          type: boolean
    MessageDetail:
      allOf:
        - $ref: "#/components/schemas/MessageSummary"
        - type: object
          description: Full message detail payload returned when `content_mode=full` or when `content_mode` is omitted.
          required: [text_body, html_body, otp, links, intent, analysis]
          properties:
            text_body:
              type: string
              nullable: true
            html_body:
              type: string
              nullable: true
            otp:
              type: string
              nullable: true
            links:
              type: array
              items:
                $ref: "#/components/schemas/ExtractedLink"
            intent:
              $ref: "#/components/schemas/MessageIntent"
            analysis:
              $ref: "#/components/schemas/MessageAnalysis"
    MessageOtpDetail:
      allOf:
        - $ref: "#/components/schemas/MessageSummary"
        - type: object
          description: Reduced message detail payload returned when `content_mode=otp`.
          required: [otp, analysis]
          properties:
            otp:
              type: string
              nullable: true
            analysis:
              $ref: "#/components/schemas/MessageAnalysis"
    MessageLinksDetail:
      allOf:
        - $ref: "#/components/schemas/MessageSummary"
        - type: object
          description: Reduced message detail payload returned when `content_mode=links`.
          required: [links, analysis]
          properties:
            links:
              type: array
              items:
                $ref: "#/components/schemas/ExtractedLink"
            analysis:
              $ref: "#/components/schemas/MessageAnalysis"
    MessageTextOnlyDetail:
      allOf:
        - $ref: "#/components/schemas/MessageSummary"
        - type: object
          description: Reduced message detail payload returned when `content_mode=text_only`.
          required: [text_body, analysis]
          properties:
            text_body:
              type: string
              nullable: true
            analysis:
              $ref: "#/components/schemas/MessageAnalysis"
    MessageAnalysis:
      type: object
      required:
        - eligible
        - status
        - requested_at
        - completed_at
        - detected_otp
        - sender_name
        - category
        - extracted_id
        - amount_mentioned
        - is_urgent
        - action_required
        - summary
      properties:
        eligible:
          type: boolean
        status:
          type: string
          enum: [none, queued, complete, failed, skipped_quota]
        requested_at:
          type: string
          nullable: true
        completed_at:
          type: string
          nullable: true
        detected_otp:
          type: string
          nullable: true
          description: Model-derived OTP candidate. The deterministic top-level `otp` remains the primary extractor output.
        sender_name:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        extracted_id:
          type: string
          nullable: true
          description: Tracking, invoice, or similar reference identifier mentioned in the email.
        amount_mentioned:
          type: string
          nullable: true
        is_urgent:
          type: boolean
          nullable: true
        action_required:
          type: boolean
          nullable: true
        summary:
          type: string
          nullable: true
    ExtractedLink:
      type: object
      required: [url, type]
      properties:
        url:
          type: string
          format: uri
        type:
          type: string
          enum: [verification, magic_link, password_reset, unsubscribe, other]
    MessageIntent:
      type: string
      nullable: true
      enum: [login_code, verification, password_reset, magic_link, invite]
    PageInfo:
      type: object
      required: [has_more, next_cursor]
      properties:
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateWebhookRequest:
      type: object
      required: [label, target_url]
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 100
        target_url:
          type: string
          format: uri
          description: HTTPS only. localhost, embedded credentials, and reserved/private IP literals are rejected.
        inbox_id:
          type: string
          nullable: true
    Webhook:
      type: object
      required:
        - id
        - inbox_id
        - label
        - target_url
        - delivery_scope
        - subscribed_events
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
        inbox_id:
          type: string
          nullable: true
        label:
          type: string
        target_url:
          type: string
        delivery_scope:
          type: string
          enum: [account, inbox]
        subscribed_events:
          type: array
          items:
            type: string
            enum: [email.received, email.enriched]
        status:
          type: string
        last_delivery_at:
          type: string
          nullable: true
        archived_at:
          type: string
          nullable: true
        created_at:
          type: string
        updated_at:
          type: string
x-postmx-webhooks:
  email.received:
    headers:
      X-PostMX-Event-Id: Webhook event identifier
      X-PostMX-Delivery-Id: Delivery attempt identifier
      X-PostMX-Timestamp: Unix timestamp used in the signature payload
      X-PostMX-Signature: v1=<base64url(hmac_sha256(signing_secret, timestamp + "." + raw_body))>
      Postmx-Signature: Duplicate of X-PostMX-Signature for Standard Webhooks compatibility
    retry_policy:
      - immediate
      - 1 minute
      - 5 minutes
      - 15 minutes
      - 1 hour
      - 6 hours
      - 24 hours
    delivery_timeout:
      10 seconds per attempt
    notes:
      - Fired immediately after message storage.
      - `data.message.analysis.status` may still be `queued`.
    payload:
      type: object
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
        type:
          type: string
          const: email.received
        created_at:
          type: string
        data:
          type: object
          required: [inbox, message]
          properties:
            inbox:
              type: object
              properties:
                id:
                  type: string
                email_address:
                  type: string
                label:
                  type: string
            message:
              $ref: "#/components/schemas/MessageDetail"
  email.enriched:
    headers:
      X-PostMX-Event-Id: Webhook event identifier
      X-PostMX-Delivery-Id: Delivery attempt identifier
      X-PostMX-Timestamp: Unix timestamp used in the signature payload
      X-PostMX-Signature: v1=<base64url(hmac_sha256(signing_secret, timestamp + "." + raw_body))>
      Postmx-Signature: Duplicate of X-PostMX-Signature for Standard Webhooks compatibility
    retry_policy:
      - immediate
      - 1 minute
      - 5 minutes
      - 15 minutes
      - 1 hour
      - 6 hours
      - 24 hours
    delivery_timeout:
      10 seconds per attempt
    notes:
      - Fired only after background message analysis completes successfully.
      - `data.message.analysis.status` is `complete` for this event.
    payload:
      type: object
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
        type:
          type: string
          const: email.enriched
        created_at:
          type: string
        data:
          type: object
          required: [inbox, message]
          properties:
            inbox:
              type: object
              properties:
                id:
                  type: string
                email_address:
                  type: string
                label:
                  type: string
            message:
              $ref: "#/components/schemas/MessageDetail"
