openapi: 3.1.0
info:
  title: Ranla API
  version: 0.10.0
  description: |
    Transactional email API for developers.
    Auth: `Authorization: Bearer rnl_…` or `stx_…` (or dashboard session cookie for integrations).
    Production base URL: https://api.ranla.ai
tags:
  - name: ApiKeys
    description: Manage API keys (list, create, revoke)
  - name: Deliverability
    description: Best-effort metrics computed from stored email events
  - name: Domains
  - name: Inbound
    description: Receive domains, inbound ingestion, and received email retrieval
  - name: Emails
  - name: Events
    description: Custom events that trigger transactional automations
  - name: Automations
    description: Create and manage event-driven transactional automations
  - name: Contacts
    description: People you can market to, with the product attributes you sync up
  - name: ContactLists
    description: Named buckets of contacts, maintained by hand or by API
  - name: Conversions
    description: |
      Report what your email actually earned. Each conversion is attributed to
      the last campaign that touched the person — by click id when you have one,
      otherwise by their most recent click, open, or send inside the goal's
      attribution window.
  - name: Integrations
  - name: Suppressions
    description: Account-level recipient suppressions (bounce/complaint/manual)
  - name: Templates
    description: Email templates with `{{variable}}` interpolation
  - name: Webhooks
servers:
  - url: https://api.ranla.ai
    description: Production
paths:
  /integrations/cloudflare:
    get:
      operationId: getCloudflareIntegration
      summary: Get Cloudflare integration status
      tags: [Integrations]
      description: Session cookie auth (dashboard). Never returns the raw API token.
      responses:
        '200':
          description: Connection status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareIntegrationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    put:
      operationId: putCloudflareIntegration
      summary: Save Cloudflare API token
      tags: [Integrations]
      description: Session cookie auth. Token is encrypted at rest.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [apiToken]
              properties:
                apiToken:
                  type: string
                  description: Cloudflare API token with Zone DNS Edit
      responses:
        '200':
          description: Token saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareIntegrationResponse'
        '400':
          description: Missing token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '503':
          description: Encryption key not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    delete:
      operationId: deleteCloudflareIntegration
      summary: Disconnect Cloudflare
      tags: [Integrations]
      description: Session cookie auth. Clears the stored token.
      responses:
        '200':
          description: Disconnected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloudflareIntegrationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
  /integrations/vercel:
    get:
      operationId: getVercelIntegration
      summary: Get Vercel integration status
      tags: [Integrations]
      description: Session cookie auth (dashboard). Never returns the raw API token.
      responses:
        '200':
          description: Connection status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VercelIntegrationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    put:
      operationId: putVercelIntegration
      summary: Save Vercel API token
      tags: [Integrations]
      description: Session cookie auth. Token is encrypted at rest.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [apiToken]
              properties:
                apiToken:
                  type: string
                  description: Vercel API token with DNS access
                teamId:
                  type: string
                  nullable: true
                  description: Optional Vercel team id for team-scoped domains
      responses:
        '200':
          description: Token saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VercelIntegrationResponse'
        '400':
          description: Missing token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '503':
          description: Encryption key not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    delete:
      operationId: deleteVercelIntegration
      summary: Disconnect Vercel
      tags: [Integrations]
      description: Session cookie auth. Clears the stored token.
      responses:
        '200':
          description: Disconnected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VercelIntegrationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
  /integrations/twilio:
    get:
      operationId: getTwilioIntegration
      summary: Get Twilio integration status
      tags: [Integrations]
      description: |
        Bearer `rnl_…` (full-access key) or dashboard session cookie.
        Never returns raw Account SID or Auth Token.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Connection status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwilioIntegrationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '403':
          description: Sending-scoped API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      operationId: putTwilioIntegration
      summary: Connect Twilio for SMS automations
      tags: [Integrations]
      description: |
        Bearer `rnl_…` (full-access key) or dashboard session cookie.
        Validates credentials against Twilio, then encrypts them at rest.
        Provide `defaultFrom` and/or `messagingServiceSid` for `send_sms` automation steps.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwilioConnectRequest'
      responses:
        '200':
          description: Twilio connected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwilioIntegrationResponse'
        '400':
          description: Validation or Twilio rejected credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '403':
          description: Sending-scoped API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Encryption key not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    delete:
      operationId: deleteTwilioIntegration
      summary: Disconnect Twilio
      tags: [Integrations]
      description: Bearer `rnl_…` (full-access key) or dashboard session cookie.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Disconnected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwilioIntegrationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '403':
          description: Sending-scoped API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /domains:
    get:
      operationId: listDomains
      summary: List sending domains
      tags: [Domains]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Maximum number of domains to return (1–100)
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor for the next page
        - name: inbound_enabled
          in: query
          schema:
            type: boolean
          description: Filter by whether inbound receiving is enabled for the domain
      responses:
        '200':
          description: Domain list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDomainsResponse'
        '401':
          description: Missing or invalid API key / session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    post:
      operationId: createDomain
      summary: Add a sending domain
      tags: [Domains]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDomainRequest'
      responses:
        '200':
          description: Domain created with DNS records to add
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateDomainResponse'
        '400':
          description: Invalid domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '403':
          description: Plan domain limit reached (when billing is enabled)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: Domain limit reached (1 on the Free plan). Upgrade at /settings/billing.
                  code: domain_limit
                  upgrade_url: /settings/billing
        '409':
          description: Domain already added on this team, or already owned by another team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: Domain already belongs to another Ranla team. Remove it from the current team or contact support to claim it.
                  code: conflict
                  details:
                    reason: domain_claim_required
                    domain: example.com
                    instructions:
                      - Ask the current Ranla team admin to remove the domain if you still have access.
                      - If you own the domain but no longer control the original team, contact support and include proof of DNS control.
  /domains/{id}:
    get:
      operationId: getDomain
      summary: Get a domain and DNS records
      tags: [Domains]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Domain id or domain name
          schema:
            type: string
      responses:
        '200':
          description: Domain detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainDetailResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '404':
          description: Domain not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    post:
      operationId: domainAction
      summary: Verify domain DNS or apply DNS via Cloudflare, Vercel, or GoDaddy
      tags: [Domains]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Domain id or domain name
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [verify, apply]
                provider:
                  type: string
                  enum: [cloudflare, godaddy, vercel]
                  description: Required when action is apply
                credentials:
                  $ref: '#/components/schemas/RemoteApplyDnsCredentials'
      responses:
        '200':
          description: Domain verified, or DNS apply results
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/VerifyDomainResponse'
                  - $ref: '#/components/schemas/ApplyDomainDnsResponse'
        '400':
          description: >
            DNS not fully ready yet (propagation — response includes pending:true and checks),
            missing provider credentials, or invalid action
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/VerifyDomainErrorResponse'
                  - $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '404':
          description: Domain not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '502':
          description: Domain registration failed, or DNS provider API error during apply
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/VerifyDomainErrorResponse'
                  - $ref: '#/components/schemas/SimpleErrorResponse'
        '503':
          description: Credentials encryption key not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    patch:
      operationId: updateDomain
      summary: Update domain tracking and TLS preferences
      tags: [Domains]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Domain id or domain name
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDomainRequest'
      responses:
        '200':
          description: Domain settings updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainDetailResponse'
        '400':
          description: Invalid JSON or settings payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '404':
          description: Domain not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    delete:
      operationId: deleteDomain
      summary: Delete a domain
      description: |
        Removes the domain from the account and revokes its sending authorization.
        If mail-server teardown fails, the domain row is kept and the API returns 502.
      tags: [Domains]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Domain deleted
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '404':
          description: Domain not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '502':
          description: Failed to revoke sending authorization upstream
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
  /deliverability:
    get:
      operationId: getDeliverability
      summary: Get deliverability insights for recent sends
      tags: [Deliverability]
      security:
        - bearerAuth: []
      parameters:
        - name: window
          in: query
          required: false
          schema:
            type: string
            enum: [7d, 30d]
            default: 30d
      responses:
        '200':
          description: Deliverability insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeliverabilityInsightsResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /events:
    post:
      operationId: triggerEvent
      summary: Ingest a custom event for automations
      tags: [Events]
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Optional idempotency key for replay-safe retries.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TriggerEventRequest'
            examples:
              userCreated:
                value:
                  name: user.created
                  user_id: user_123
                  email: user@example.com
                  data:
                    name: Ada Lovelace
      responses:
        '202':
          description: Event accepted for automation processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerEventResponse'
        '400':
          description: Invalid event payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key / session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /automations:
    get:
      operationId: listAutomations
      summary: List automations
      tags: [Automations]
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Automation list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAutomationsResponse'
        '401':
          description: Missing or invalid API key / session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createAutomation
      summary: Create a draft automation
      tags: [Automations]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAutomationRequest'
            examples:
              welcome:
                value:
                  name: Welcome email
                  trigger:
                    type: event
                    event: user.created
                  steps:
                    - type: send_email
                      email:
                        from: you@yourdomain.com
                        to: '{{event.email}}'
                        subject: Welcome
                        html: '<p>Hi {{event.data.name}}</p>'
      responses:
        '201':
          description: Automation created as draft
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationResponse'
        '400':
          description: Invalid automation definition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key / session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Automation name already in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /automations/{id}:
    get:
      operationId: getAutomation
      summary: Get an automation
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Automation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateAutomation
      summary: Update an automation definition
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAutomationRequest'
      responses:
        '200':
          description: Updated automation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationResponse'
        '400':
          description: Invalid update
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Name conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: deleteAutomation
      summary: Delete an automation
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: automationAction
      summary: Activate or pause an automation
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutomationActionRequest'
      responses:
        '200':
          description: Updated automation status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationResponse'
        '400':
          description: Invalid action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /automations/runs:
    get:
      operationId: listAutomationRuns
      summary: List automation runs
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: automation_id
          in: query
          required: false
          schema:
            type: string
          description: Filter runs to a single automation
      responses:
        '200':
          description: Run list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAutomationRunsResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /automations/runs/{id}:
    get:
      operationId: getAutomationRun
      summary: Get an automation run
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Run detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationRunResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: automationRunAction
      summary: Cancel or retry an automation run
      tags: [Automations]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutomationRunActionRequest'
      responses:
        '200':
          description: Updated run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutomationRunResponse'
        '400':
          description: Invalid action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /emails:
    get:
      operationId: listEmails
      summary: List recent sent emails
      tags: [Emails]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Maximum number of emails to return (1–100)
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor for the next page
      responses:
        '200':
          description: Email list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListEmailsResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: sendEmail
      summary: Send a transactional email
      tags: [Emails]
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: |
            Optional idempotency key (max 256 chars). Retries with the same key
            and identical body return the original response. Same key with a
            different body, or a concurrent in-flight request, returns 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
            examples:
              basic:
                value:
                  from: you@yourdomain.com
                  to: user@example.com
                  subject: Hello from Ranla
                  html: '<p>It works.</p>'
      responses:
        '200':
          description: Email accepted for delivery
          headers:
            ratelimit-limit:
              description: Request quota for the current window
              schema:
                type: integer
            ratelimit-remaining:
              description: Remaining requests in the current window
              schema:
                type: integer
            ratelimit-reset:
              description: Unix timestamp when the current window resets
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: From domain not verified, sandbox/verified-domain sender used for non-account recipients, or send to other recipients without a paid plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Idempotency key conflict (payload mismatch or concurrent request)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: Idempotency key was already used with a different request body.
                  code: idempotency_conflict
        '422':
          description: Recipient is suppressed or opted out of the email category
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                category_unsubscribed:
                  summary: Category opt-out
                  value:
                    error:
                      message: One or more recipients opted out of this email category
                      code: validation_error
                      details:
                        category_unsubscribed: ['user@example.com']
                        category: product
                        email_id: msg_…
                suppressed:
                  summary: Global suppression
                  value:
                    error:
                      message: One or more recipients are suppressed
                      code: validation_error
                      details:
                        suppressed: ['user@example.com']
                        email_id: msg_…
        '429':
          description: Free plan send limit exceeded (daily or monthly; paid plans meter overage instead)
          headers:
            ratelimit-limit:
              description: Request quota for the current window
              schema:
                type: integer
            ratelimit-remaining:
              description: Remaining requests in the current window
              schema:
                type: integer
            ratelimit-reset:
              description: Unix timestamp when the current window resets
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: Monthly send limit reached (3000/month on the free plan). Upgrade at /settings/billing.
                  code: plan_limit
                  upgrade_url: /settings/billing
        '502':
          description: Upstream mail server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Mail server not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /emails/batch:
    post:
      operationId: sendEmailBatch
      summary: Send a batch of transactional emails
      tags: [Emails]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchSendEmailRequest'
      responses:
        '200':
          description: Index-aligned per-message results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchSendEmailResponse'
        '400':
          description: Invalid batch wrapper
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Mail server not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /emails/{id}:
    get:
      operationId: getEmail
      summary: Retrieve a sent email
      tags: [Emails]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Public message ID (`msg_…`)
          schema:
            type: string
      responses:
        '200':
          description: Email detail
          content:
            application/json:
              schema:
                type: object
                required: [email]
                properties:
                  email:
                    $ref: '#/components/schemas/EmailListItem'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Email not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateScheduledEmail
      summary: Reschedule or cancel a scheduled email
      tags: [Emails]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Public message ID (`msg_…`)
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateScheduledEmailRequest'
            examples:
              reschedule:
                value:
                  scheduled_at: '2026-08-01T12:00:00.000Z'
              cancel:
                value:
                  cancel: true
      responses:
        '200':
          description: Scheduled email updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
        '400':
          description: Email is not scheduled, or request is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Email not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /emails/{id}/resend:
    post:
      operationId: resendEmail
      summary: Resend a previously stored email
      description: |
        Creates a **new** send using the stored from/to/subject/body (and tags/headers)
        from an existing message. Returns a new `msg_…` id. Attachment bytes are not
        stored, so attachments are omitted on resend.
      tags: [Emails]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Public message ID of the source email (`msg_…`)
          schema:
            type: string
      responses:
        '200':
          description: New email accepted for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
        '400':
          description: Source email has no stored body, or request is invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: From domain is not verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Source email not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Mail server not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /received-emails:
    get:
      operationId: listReceivedEmails
      summary: List received emails
      tags: [Inbound]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: domain
          in: query
          schema:
            type: string
          description: Filter by receiving domain name
      responses:
        '200':
          description: Received email list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListReceivedEmailsResponse'
        '401':
          description: Missing or invalid API key / session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /received-emails/{id}:
    get:
      operationId: getReceivedEmail
      summary: Retrieve a received email with attachment data
      tags: [Inbound]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Received email public id (`inb_…`)
          schema:
            type: string
      responses:
        '200':
          description: Received email detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceivedEmailResponse'
        '401':
          description: Missing or invalid API key / session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Received email not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /webhooks:
    get:
      operationId: listWebhooks
      summary: List webhook endpoints
      tags: [Webhooks]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Maximum number of webhooks to return (1–100)
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor for the next page
      responses:
        '200':
          description: Webhook endpoint list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWebhooksResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createWebhook
      summary: Create a webhook endpoint
      tags: [Webhooks]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '200':
          description: Webhook created (signing secret shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateWebhookResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /webhooks/{id}:
    get:
      operationId: getWebhook
      summary: Retrieve a webhook endpoint
      tags: [Webhooks]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Webhook endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateWebhook
      summary: Update a webhook endpoint
      tags: [Webhooks]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
      responses:
        '200':
          description: Webhook updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
    delete:
      operationId: deleteWebhook
      summary: Delete a webhook endpoint
      tags: [Webhooks]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleErrorResponse'
  /conversions:
    post:
      operationId: createConversion
      summary: Record a conversion
      tags: [Conversions]
      description: |
        Idempotent on `(goal, external_id)` — send the same `external_id` twice
        and the second call returns the first conversion with `duplicate: true`,
        so a retried payment webhook can never double-count revenue.

        Identify the person with whichever of `click_id`, `contact_id`, or
        `email` you have. `click_id` is the `stx_cid` value carried on links in
        campaign email and stored by the tracking script; it is the strongest
        signal and skips the lookup entirely. Recording a conversion never
        creates a contact.

        Omit `goal` to use the account's primary goal.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConversionRequest'
      responses:
        '200':
          description: Conversion recorded, or an existing one returned unchanged
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversionResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Conversion goal not found, and no primary goal is configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      operationId: listConversions
      summary: List conversions
      tags: [Conversions]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: goal
          in: query
          schema:
            type: string
          description: Filter by goal key
        - name: goal_id
          in: query
          schema:
            type: string
        - name: campaign_id
          in: query
          schema:
            type: string
          description: Only conversions attributed to this campaign
        - name: email
          in: query
          schema:
            type: string
            format: email
      responses:
        '200':
          description: Conversion list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListConversionsResponse'
        '400':
          description: Invalid cursor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /suppressions:
    get:
      operationId: listSuppressions
      summary: List suppressions
      tags: [Suppressions]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: email
          in: query
          schema:
            type: string
            format: email
          description: Filter by normalized email address
      responses:
        '200':
          description: Suppression list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListSuppressionsResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createSuppression
      summary: Add an email to the suppression list
      tags: [Suppressions]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSuppressionRequest'
      responses:
        '200':
          description: Suppression created or updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuppressionResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: deleteSuppressionByEmail
      summary: Remove a suppression by email query parameter
      tags: [Suppressions]
      security:
        - bearerAuth: []
      parameters:
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
      responses:
        '200':
          description: Suppression removed
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Suppression not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /suppressions/{id}:
    delete:
      operationId: deleteSuppression
      summary: Remove a suppression by id
      tags: [Suppressions]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Suppression removed
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Suppression not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /contacts:
    get:
      operationId: listContacts
      summary: List contacts
      tags: [Contacts]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
          description: Matches email, first name, last name, or external id
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/ContactStatus'
        - name: list_id
          in: query
          schema:
            type: string
          description: Only contacts belonging to this list
      responses:
        '200':
          description: Contact list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListContactsResponse'
        '400':
          description: Invalid cursor or status filter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: upsertContact
      summary: Create or update a contact
      description: |
        Idempotent on the email address. `attributes` is merged into whatever is
        already stored, so a partial sync never wipes state you set earlier.
        Setting `status` to `unsubscribed` records a marketing opt-out and emits
        the `contact.unsubscribed` webhook; transactional mail is unaffected.
      tags: [Contacts]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertContactRequest'
      responses:
        '200':
          description: Contact created or updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /contacts/import:
    post:
      operationId: importContacts
      summary: Bulk import contacts
      description: |
        Accepts a JSON array of contacts, `{ "contacts": [...] }`, `{ "csv": "…" }`,
        or a raw CSV body. CSV columns other than `email`, `external_id`,
        `first_name`, and `last_name` are stored as attributes. Malformed
        addresses are counted in `invalid` rather than failing the batch.
      tags: [Contacts]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportContactsRequest'
          text/csv:
            schema:
              type: string
              description: CSV text with a header row containing an `email` column
      responses:
        '200':
          description: Import summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportContactsResponse'
        '400':
          description: Unreadable body, or more rows than one request allows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /contacts/{id}:
    get:
      operationId: getContact
      summary: Retrieve a contact
      tags: [Contacts]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact id or email address
      responses:
        '200':
          description: Contact
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateContact
      summary: Update a contact
      tags: [Contacts]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact id or email address
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContactRequest'
      responses:
        '200':
          description: Contact updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: deleteContact
      summary: Delete a contact
      tags: [Contacts]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact id or email address
      responses:
        '200':
          description: Contact deleted
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /contact-lists:
    get:
      operationId: listContactLists
      summary: List contact lists
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Contact lists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListContactListsResponse'
        '400':
          description: Invalid cursor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createContactList
      summary: Create a contact list
      description: The `key` is a slug derived from the name and is stable once created.
      tags: [ContactLists]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactListRequest'
      responses:
        '200':
          description: Contact list created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactListResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A list with that name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /contact-lists/{id}:
    get:
      operationId: getContactList
      summary: Retrieve a contact list
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: List id or key
      responses:
        '200':
          description: Contact list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactListResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact list not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateContactList
      summary: Update a contact list
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: List id or key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContactListRequest'
      responses:
        '200':
          description: Contact list updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactListResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact list not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: A list with that name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: deleteContactList
      summary: Delete a contact list
      description: Deletes the list and its memberships. Contacts themselves are untouched.
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: List id or key
      responses:
        '200':
          description: Contact list deleted
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact list not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /contact-lists/{id}/contacts:
    get:
      operationId: listContactListMembers
      summary: List the contacts on a list
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: List id or key
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Contacts on the list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListContactsResponse'
        '400':
          description: Invalid cursor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact list not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: addContactsToList
      summary: Add contacts to a list
      description: Identify contacts by `contact_ids`, by `emails`, or both. Addresses we have never seen come back in `not_found`.
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: List id or key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactListMembershipRequest'
      responses:
        '200':
          description: Membership updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddContactListMembersResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact list not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: removeContactsFromList
      summary: Remove contacts from a list
      tags: [ContactLists]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: List id or key
        - name: contact_ids
          in: query
          schema:
            type: string
          description: Comma-separated contact ids, for clients that cannot send a DELETE body
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactListMembershipRequest'
      responses:
        '200':
          description: Membership updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoveContactListMembersResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Contact list not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates:
    get:
      operationId: listTemplates
      summary: List email templates
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, published]
          description: Filter by draft or published
      responses:
        '200':
          description: Template list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListTemplatesResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createTemplate
      summary: Create an email template (draft)
      tags: [Templates]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateRequest'
      responses:
        '201':
          description: Template created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Name or alias conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates/starters:
    get:
      operationId: listTemplateStarters
      summary: List starter template catalog
      description: >
        Returns the global starter catalog (password-reset, welcome, etc.).
        Copy into the account with POST /templates/from-starter.
      tags: [Templates]
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Starter catalog
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TemplateStarter'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates/from-starter:
    post:
      operationId: createTemplateFromStarter
      summary: Create a published template from a starter
      description: >
        Copies a catalog starter into the account as a published template using the
        starter alias (e.g. password-reset). Returns 409 if name or alias already exists.
      tags: [Templates]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [alias]
              properties:
                alias:
                  type: string
                  description: Starter alias from GET /templates/starters
                  example: password-reset
      responses:
        '201':
          description: Published template created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Unknown starter alias
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Name or alias already in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates/{id}:
    get:
      operationId: getTemplate
      summary: Retrieve a template by id or alias
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: Template UUID or alias
          schema:
            type: string
      responses:
        '200':
          description: Template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      operationId: updateTemplate
      summary: Update a template by id or alias
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTemplateRequest'
      responses:
        '200':
          description: Template updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Name or alias conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      operationId: deleteTemplate
      summary: Delete a template by id or alias
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Template deleted
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: templateAction
      summary: Publish or duplicate a template
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateActionRequest'
      responses:
        '200':
          description: Publish succeeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '201':
          description: Duplicate created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '400':
          description: Invalid action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates/{id}/versions:
    get:
      operationId: listTemplateVersions
      summary: List immutable published versions of a template
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Version list
          content:
            application/json:
              schema:
                type: object
                required: [data, published_version_id]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TemplateVersion'
                  published_version_id:
                    type: string
                    format: uuid
                    nullable: true
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates/{id}/versions/{version}:
    post:
      operationId: restoreTemplateVersion
      summary: Restore a historical version into the draft
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [restore]
      responses:
        '200':
          description: Draft restored from version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template or version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /templates/{id}/test-send:
    post:
      operationId: testSendTemplate
      summary: Send a one-off test using the current draft
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, from]
              properties:
                to:
                  type: string
                from:
                  type: string
                variables:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Test email accepted
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /assets:
    get:
      operationId: listAssets
      summary: List uploaded template assets
      tags: [Templates]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Asset list
          content:
            application/json:
              schema:
                type: object
                required: [data, has_more, next_cursor, upload_enabled]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Asset'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
                  upload_enabled:
                    type: boolean
    post:
      operationId: createAssetUpload
      summary: Create a presigned upload URL for a template image
      tags: [Templates]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content_type, bytes]
              properties:
                content_type:
                  type: string
                  enum: [image/png, image/jpeg, image/gif, image/webp]
                bytes:
                  type: integer
                width:
                  type: integer
                height:
                  type: integer
      responses:
        '201':
          description: Presigned upload created
          content:
            application/json:
              schema:
                type: object
                required: [asset, upload_url, upload_headers]
                properties:
                  asset:
                    $ref: '#/components/schemas/Asset'
                  upload_url:
                    type: string
                    format: uri
                  upload_headers:
                    type: object
                    additionalProperties:
                      type: string
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Uploads not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /emails/test:
    post:
      operationId: testEmailWebhookEvent
      summary: Simulate a webhook event (test sink for CI)
      tags: [Emails]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event]
              properties:
                event:
                  $ref: '#/components/schemas/EmailWebhookEventType'
                email_id:
                  type: string
                  description: Optional existing email public id (`msg_…`)
                webhook_id:
                  type: string
                  description: Optional webhook endpoint id (`wh_…`) for a single-endpoint test delivery
                sync:
                  type: boolean
                  default: false
                  description: When true, deliver synchronously and return per-endpoint HTTP results (defaults to true when `webhook_id` is set)
                deliver:
                  type: boolean
                  default: true
                  description: When true, enqueue deliveries to account webhooks
      responses:
        '200':
          description: Payload built (and optionally delivered)
          content:
            application/json:
              schema:
                type: object
                required: [ok, payload, delivered, deliveries]
                properties:
                  ok:
                    type: boolean
                  payload:
                    $ref: '#/components/schemas/EmailWebhookEventPayload'
                  delivered:
                    type: boolean
                  deliveries:
                    type: array
                    items:
                      type: object
                      required: [endpoint_id, url, success]
                      properties:
                        endpoint_id:
                          type: string
                        url:
                          type: string
                        success:
                          type: boolean
                        status:
                          type: integer
                        error:
                          type: string
        '400':
          description: Invalid event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api-keys:
    get:
      operationId: listApiKeys
      summary: List API keys
      tags: [ApiKeys]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: Maximum number of API keys to return (1–100)
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor for the next page
      responses:
        '200':
          description: API key list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListApiKeysResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createApiKey
      summary: Create an API key
      tags: [ApiKeys]
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
      responses:
        '200':
          description: API key created (token shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateApiKeyResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api-keys/{id}:
    delete:
      operationId: deleteApiKey
      summary: Revoke an API key
      tags: [ApiKeys]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: API key revoked
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: API key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /smtp-credentials:
    get:
      operationId: listSmtpCredentials
      summary: List SMTP credentials
      tags: [SmtpCredentials]
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: SMTP credential list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListSmtpCredentialsResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      operationId: createSmtpCredential
      summary: Create an SMTP credential
      tags: [SmtpCredentials]
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSmtpCredentialRequest'
      responses:
        '200':
          description: SMTP credential created (password shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSmtpCredentialResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /smtp-credentials/{id}:
    delete:
      operationId: deleteSmtpCredential
      summary: Revoke an SMTP credential
      tags: [SmtpCredentials]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: SMTP credential revoked
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: SMTP credential not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Ranla API key prefixed with `rnl_` or `stx_`
  schemas:
    SendEmailRequest:
      type: object
      required: [from, to]
      description: |
        `subject` is required unless `template` is provided (subject comes from the published template).
        Provide `html` and/or `text`, or `template` (mutually exclusive). React Email is compiled
        client-side by the Node SDK (`react` option) — do not send `react` to the HTTP API.
      properties:
        from:
          oneOf:
            - type: string
              format: email
            - type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                name:
                  type: string
        to:
          oneOf:
            - type: string
              format: email
            - type: array
              items:
                type: string
                format: email
        subject:
          type: string
          description: Required unless using `template` (optional override when sending with a template).
        html:
          type: string
        htmlBody:
          type: string
          description: Alias for html
        html_body:
          type: string
          description: Alias for html
        text:
          type: string
        textBody:
          type: string
          description: Alias for text
        text_body:
          type: string
          description: Alias for text
        plain_body:
          type: string
          description: Alias for text
        template:
          $ref: '#/components/schemas/SendTemplateRef'
        reply_to:
          oneOf:
            - type: string
              format: email
            - type: array
              items:
                type: string
                format: email
        replyTo:
          oneOf:
            - type: string
              format: email
            - type: array
              items:
                type: string
                format: email
          description: Alias for reply_to
        cc:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        bcc:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        tags:
          oneOf:
            - type: array
              maxItems: 10
              items:
                $ref: '#/components/schemas/EmailTag'
            - type: object
              additionalProperties:
                type: string
          description: Key/value tags stored with the email. Ranla derives a single tag for delivery metadata.
        tag:
          type: string
          description: Optional delivery tag override.
        headers:
          type: object
          additionalProperties:
            type: string
          description: Custom headers after server-side safety filtering.
        attachments:
          type: array
          maxItems: 10
          description: Not supported for batch sends or scheduled sends in v1.
          items:
            $ref: '#/components/schemas/EmailAttachment'
        scheduled_at:
          type: string
          format: date-time
          description: Schedule delivery for a future ISO 8601 timestamp. Returns status `scheduled`.
        scheduledAt:
          type: string
          format: date-time
          description: Alias for scheduled_at.
        unsubscribe:
          type: boolean
          description: |
            Opt in to managed unsubscribe for this send. Injects RFC 8058 `List-Unsubscribe`
            headers and replaces `{{unsubscribe_url}}` in subject/html/text. When sending with
            a template, inherits the template's `unsubscribe_enabled` setting unless overridden here.
        category:
          type: string
          enum: [transactional, product, newsletter]
          description: |
            Optional send-level category override. Defaults to the template's category when sending
            with a template, otherwise `transactional`. Product and newsletter categories support
            category-scoped opt-out and auto-enable managed unsubscribe.
    SendTemplateRef:
      type: object
      required: [id]
      description: Reference a published template by UUID or alias. Mutually exclusive with html/text.
      properties:
        id:
          type: string
          description: Template UUID or alias
        variables:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
          description: Values for `{{variable}}` placeholders (HTML-escaped in html body)
    EmailTag:
      type: object
      required: [name, value]
      properties:
        name:
          type: string
        value:
          type: string
    EmailAttachment:
      type: object
      required: [name, content_type, data]
      properties:
        name:
          type: string
          description: Filename sent to the recipient.
        filename:
          type: string
          description: Alias for name.
        content_type:
          type: string
          example: application/pdf
        contentType:
          type: string
          description: Alias for content_type.
        data:
          type: string
          format: byte
          description: Base64-encoded attachment content (10MB decoded max).
        content:
          type: string
          format: byte
          description: Alias for data (base64 string expected over HTTP).
        content_id:
          type: string
          description: Optional Content-ID for inline attachments.
        contentId:
          type: string
          description: Alias for content_id.
    BatchSendEmailRequest:
      type: object
      required: [emails]
      properties:
        emails:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/SendEmailRequest'
          description: Attachments are rejected for batch sends in v1.
    BatchSendEmailResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            type: object
            required: [index]
            properties:
              index:
                type: integer
              id:
                type: string
              status:
                type: string
              error:
                type: object
                properties:
                  message:
                    type: string
                  code:
                    type: string
                  status_code:
                    type: integer
                  details: {}
    UpdateScheduledEmailRequest:
      type: object
      properties:
        scheduled_at:
          type: string
          format: date-time
        scheduledAt:
          type: string
          format: date-time
          description: Alias for scheduled_at.
        cancel:
          type: boolean
    SendEmailResponse:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
          description: Public message ID (`msg_…`)
        status:
          type: string
          example: sent
    EmailListItem:
      type: object
      required: [id, from, to, subject, status, created_at]
      properties:
        id:
          type: string
        from:
          type: string
        to:
          type: array
          items:
            type: string
            format: email
        cc:
          type: array
          items:
            type: string
            format: email
        bcc:
          type: array
          items:
            type: string
            format: email
        reply_to:
          type: array
          items:
            type: string
            format: email
        subject:
          type: string
        status:
          type: string
          enum: [queued, scheduled, sent, delivered, bounced, failed, cancelled]
        last_event:
          type: string
          nullable: true
        bounce_reason:
          type: string
          nullable: true
        tags:
          oneOf:
            - type: array
              items:
                $ref: '#/components/schemas/EmailTag'
            - type: object
          nullable: true
        scheduled_at:
          type: string
          format: date-time
          nullable: true
        cancelled_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        sent_at:
          type: string
          format: date-time
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        bounced_at:
          type: string
          format: date-time
          nullable: true
    ListEmailsResponse:
      type: object
      required: [emails, has_more, next_cursor]
      properties:
        emails:
          type: array
          items:
            $ref: '#/components/schemas/EmailListItem'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    ReceivedEmailAttachmentMeta:
      type: object
      required: [filename, content_type, size]
      properties:
        filename:
          type: string
        content_type:
          type: string
        size:
          type: integer
        content_id:
          type: string
          nullable: true
    ReceivedEmailAttachment:
      allOf:
        - $ref: '#/components/schemas/ReceivedEmailAttachmentMeta'
        - type: object
          properties:
            data:
              type: string
              format: byte
              description: Base64-encoded attachment content. Present on retrieve responses, omitted from list responses.
    ReceivedEmail:
      type: object
      required:
        [id, domain_id, domain_name, status, rcpt_to, mail_from, from, to, cc, subject, spam_status, bounced, received_with_ssl, message_id, in_reply_to, references, attachment_count, attachments, text_body, html_body, auto_submitted, received_at, created_at]
      properties:
        id:
          type: string
          description: Received email public id (`inb_…`)
        domain_id:
          type: string
        domain_name:
          type: string
        status:
          type: string
          enum: [received]
        rcpt_to:
          type: string
          format: email
          description: Envelope recipient as received by Ranla.
        mail_from:
          type: string
          nullable: true
          description: SMTP envelope sender.
        from:
          type: string
        to:
          type: array
          items:
            type: string
            format: email
        cc:
          type: array
          items:
            type: string
            format: email
        subject:
          type: string
        spam_status:
          type: string
          nullable: true
        bounced:
          type: boolean
        received_with_ssl:
          type: boolean
          nullable: true
        message_id:
          type: string
          nullable: true
        in_reply_to:
          type: string
          nullable: true
        references:
          type: array
          items:
            type: string
        attachment_count:
          type: integer
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/ReceivedEmailAttachment'
        text_body:
          type: string
          nullable: true
        html_body:
          type: string
          nullable: true
        auto_submitted:
          type: string
          nullable: true
        received_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    ListReceivedEmailsResponse:
      type: object
      required: [received_emails, has_more, next_cursor]
      properties:
        received_emails:
          type: array
          items:
            $ref: '#/components/schemas/ReceivedEmail'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    ReceivedEmailResponse:
      type: object
      required: [received_email]
      properties:
        received_email:
          $ref: '#/components/schemas/ReceivedEmail'
    DeliverabilityBucket:
      type: object
      required:
        - total_emails
        - attempted
        - sent
        - delivered
        - bounced
        - complained
        - failed
        - suppressed
        - scheduled
        - opened
        - clicked
        - bounce_rate
        - complaint_rate
        - delivery_rate
        - open_rate
        - click_rate
      properties:
        total_emails:
          type: integer
        attempted:
          type: integer
        sent:
          type: integer
        delivered:
          type: integer
        bounced:
          type: integer
        complained:
          type: integer
        failed:
          type: integer
        suppressed:
          type: integer
        scheduled:
          type: integer
        opened:
          type: integer
        clicked:
          type: integer
        bounce_rate:
          type: number
        complaint_rate:
          type: number
        delivery_rate:
          type: number
        open_rate:
          type: number
        click_rate:
          type: number
    DeliverabilityMailServerRelay:
      type: object
      required:
        - window_days
        - total_messages
        - sent
        - bounced
        - failed
        - delayed
      properties:
        window_days:
          type: integer
          description: Lookback window in days for relay-side counts.
        total_messages:
          type: integer
        sent:
          type: integer
        bounced:
          type: integer
        failed:
          type: integer
        delayed:
          type: integer
      description: Optional relay-side delivery counts from the transactional mail server when relay metrics are available.
    DeliverabilityDomainInsight:
      allOf:
        - $ref: '#/components/schemas/DeliverabilityBucket'
        - type: object
          required: [domain]
          properties:
            domain:
              type: string
            mail_server_relay:
              $ref: '#/components/schemas/DeliverabilityMailServerRelay'
    DeliverabilityInsightsResponse:
      type: object
      required:
        - window
        - window_start
        - generated_at
        - summary
        - by_domain
        - top_bounce_reasons
        - notes
      properties:
        window:
          type: string
          enum: [7d, 30d]
        window_start:
          type: string
          format: date-time
        generated_at:
          type: string
          format: date-time
        summary:
          $ref: '#/components/schemas/DeliverabilityBucket'
        by_domain:
          type: array
          items:
            $ref: '#/components/schemas/DeliverabilityDomainInsight'
        top_bounce_reasons:
          type: array
          items:
            type: object
            required: [reason, count]
            properties:
              reason:
                type: string
              count:
                type: integer
        notes:
          type: array
          items:
            type: string
    PaginationMeta:
      type: object
      required: [has_more, next_cursor]
      properties:
        has_more:
          type: boolean
          description: Whether another page of results is available
        next_cursor:
          type: string
          nullable: true
          description: Cursor to pass as `cursor` for the next page, or null when has_more is false
    EmailWebhookEventType:
      type: string
      enum:
        - email.received
        - email.sent
        - email.delivered
        - email.delivery_delayed
        - email.bounced
        - email.complained
        - email.opened
        - email.clicked
        - email.failed
        - email.suppressed
        - email.scheduled
    WebhookEventType:
      type: string
      enum:
        - email.received
        - email.sent
        - email.delivered
        - email.delivery_delayed
        - email.bounced
        - email.complained
        - email.opened
        - email.clicked
        - email.failed
        - email.suppressed
        - email.scheduled
        - contact.unsubscribed
        - automation.started
        - automation.step_completed
        - automation.completed
        - automation.failed
    Suppression:
      type: object
      required: [id, email, reason, source, created_at, updated_at]
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        reason:
          type: string
          nullable: true
        source:
          type: string
          enum: [bounce, complaint, manual, api, unsubscribe]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ListSuppressionsResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Suppression'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateSuppressionRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        reason:
          type: string
    SuppressionResponse:
      type: object
      required: [suppression]
      properties:
        suppression:
          $ref: '#/components/schemas/Suppression'
    AttributionKind:
      type: string
      enum: [click, open, send, none]
      description: |
        How the conversion was tied to a campaign, strongest first. Reported so
        you can discount weak attribution rather than treat every conversion as
        earned: `click` came from a click id, `open` and `send` from the last
        campaign that reached the person inside the goal's window, and `none`
        means the conversion is unattributed.
    Conversion:
      type: object
      required:
        [
          id,
          goal_id,
          contact_id,
          email,
          value_cents,
          currency,
          external_id,
          source,
          occurred_at,
          attributed_campaign_id,
          attributed_variant_id,
          attributed_recipient_id,
          attribution_kind,
          created_at,
        ]
      properties:
        id:
          type: string
        goal_id:
          type: string
        contact_id:
          type: string
          nullable: true
          description: Null when no contact matched; the email is kept either way.
        email:
          type: string
          format: email
          nullable: true
        value_cents:
          type: integer
          description: Minor units of `currency`. Always 0 for a `visit` goal.
        currency:
          type: string
        external_id:
          type: string
          nullable: true
        source:
          type: string
          enum: [api, script, event]
        occurred_at:
          type: string
          format: date-time
        attributed_campaign_id:
          type: string
          nullable: true
        attributed_variant_id:
          type: string
          nullable: true
        attributed_recipient_id:
          type: string
          nullable: true
        attribution_kind:
          $ref: '#/components/schemas/AttributionKind'
        created_at:
          type: string
          format: date-time
    CreateConversionRequest:
      type: object
      description: Identify the person with at least one of `click_id`, `contact_id`, or `email`.
      properties:
        goal:
          type: string
          description: Goal key. Defaults to the account's primary goal.
        goal_id:
          type: string
        click_id:
          type: string
          description: The `stx_cid` value carried on campaign links.
        contact_id:
          type: string
        email:
          type: string
          format: email
        value_cents:
          type: integer
          minimum: 0
          description: |
            Amount in minor units (cents). Defaults to the goal's default value.
            Named `value_cents` so `49` cannot be mistaken for $49.
        currency:
          type: string
          description: 3-letter ISO code. Defaults to the goal's currency.
        external_id:
          type: string
          description: |
            Your own id for this conversion (charge id, order number). Makes the
            call idempotent for the goal.
        occurred_at:
          type: string
          format: date-time
          description: Defaults to now. Attribution is measured back from this time.
    ConversionResponse:
      type: object
      required: [conversion, duplicate]
      properties:
        conversion:
          $ref: '#/components/schemas/Conversion'
        duplicate:
          type: boolean
          description: |
            True when this `external_id` was already recorded for the goal — the
            existing conversion is returned unchanged and nothing was written.
    ListConversionsResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Conversion'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    ContactStatus:
      type: string
      enum: [subscribed, unsubscribed, bounced, complained]
      description: |
        Derived on read. A contact who is suppressed, or who has opted out of
        newsletter mail, always reads back as `unsubscribed` — there is one
        source of truth for consent and this reflects it.
    ContactSource:
      type: string
      enum: [api, import, dashboard, agent]
    Contact:
      type: object
      required:
        - id
        - email
        - external_id
        - first_name
        - last_name
        - attributes
        - status
        - source
        - last_engaged_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        external_id:
          type: string
          nullable: true
          description: Your own id for this person, for joins back into your product
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        attributes:
          type: object
          additionalProperties: true
          description: Flat, scalar-valued product state you sync up. Segments filter on it.
        status:
          $ref: '#/components/schemas/ContactStatus'
        source:
          $ref: '#/components/schemas/ContactSource'
        last_engaged_at:
          type: string
          format: date-time
          nullable: true
          description: Last open or click across any campaign
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ListContactsResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Contact'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    ContactResponse:
      type: object
      required: [contact]
      properties:
        contact:
          $ref: '#/components/schemas/Contact'
    UpsertContactRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        external_id:
          type: string
          nullable: true
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        attributes:
          type: object
          additionalProperties: true
          nullable: true
          description: Merged into existing attributes. Send `null` to clear them.
        status:
          $ref: '#/components/schemas/ContactStatus'
        source:
          $ref: '#/components/schemas/ContactSource'
    UpdateContactRequest:
      type: object
      properties:
        external_id:
          type: string
          nullable: true
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        attributes:
          type: object
          additionalProperties: true
          nullable: true
          description: Merged into existing attributes. Send `null` to clear them.
        status:
          $ref: '#/components/schemas/ContactStatus'
    ContactImportRow:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        external_id:
          type: string
          nullable: true
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        attributes:
          type: object
          additionalProperties: true
    ImportContactsRequest:
      oneOf:
        - type: array
          items:
            $ref: '#/components/schemas/ContactImportRow'
        - type: object
          properties:
            contacts:
              type: array
              items:
                $ref: '#/components/schemas/ContactImportRow'
            csv:
              type: string
              description: CSV text with a header row containing an `email` column
            source:
              $ref: '#/components/schemas/ContactSource'
    ImportContactsResponse:
      type: object
      required: [created, updated, invalid]
      properties:
        created:
          type: integer
        updated:
          type: integer
        invalid:
          type: integer
          description: Rows whose email address was malformed. They are skipped, not fatal.
    ContactList:
      type: object
      required: [id, name, key, description, contact_count, created_at, updated_at]
      properties:
        id:
          type: string
        name:
          type: string
        key:
          type: string
          description: URL-safe slug derived from the name, stable once created
        description:
          type: string
          nullable: true
        contact_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ListContactListsResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ContactList'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    ContactListResponse:
      type: object
      required: [list]
      properties:
        list:
          $ref: '#/components/schemas/ContactList'
    CreateContactListRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
    UpdateContactListRequest:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
    ContactListMembershipRequest:
      type: object
      properties:
        contact_ids:
          type: array
          items:
            type: string
        emails:
          type: array
          items:
            type: string
            format: email
    AddContactListMembersResponse:
      type: object
      required: [added, contact_count, not_found]
      properties:
        added:
          type: integer
        contact_count:
          type: integer
          description: Size of the list after the change
        not_found:
          type: array
          items:
            type: string
          description: Addresses that do not match a contact on this account
    RemoveContactListMembersResponse:
      type: object
      required: [removed, contact_count, not_found]
      properties:
        removed:
          type: integer
        contact_count:
          type: integer
          description: Size of the list after the change
        not_found:
          type: array
          items:
            type: string
    Template:
      type: object
      required:
        - id
        - name
        - alias
        - status
        - format
        - subject
        - html
        - text
        - variables
        - published_at
        - created_at
        - updated_at
        - unsubscribe_enabled
        - category
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        alias:
          type: string
          nullable: true
          description: Optional unique alias per account (must not start with `rnl_` or `stx_`)
        status:
          type: string
          enum: [draft, published]
        format:
          type: string
          enum: [html, blocks]
          description: html is legacy raw HTML; blocks is the visual builder document
        subject:
          type: string
        preheader:
          type: string
          nullable: true
          description: Inbox preview text injected as a hidden preheader
        html:
          type: string
          nullable: true
        text:
          type: string
          nullable: true
        design:
          description: >-
            TemplateDocument JSON when format is blocks. `settings.globalCss` accepts
            author-supplied CSS; bare-tag rules (body, p, h1, h2, h3, a, img, hr) are
            inlined on render and everything else is emitted into a style block.
          nullable: true
          type: object
          additionalProperties: true
        variables:
          type: array
          nullable: true
          items:
            type: string
          description: Variable names used in subject/html/text (auto-extracted for blocks)
        published_version_id:
          type: string
          format: uuid
          nullable: true
          description: Immutable version snapshot used for sends
        published_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        unsubscribe_enabled:
          type: boolean
          description: |
            When true, sends using this published template inherit managed unsubscribe unless
            overridden with `unsubscribe` on `POST /emails`.
        category:
          type: string
          enum: [transactional, product, newsletter]
          description: |
            Email category for preference management. `transactional` is default and ignores
            category opt-outs. `product` and `newsletter` support category-scoped unsubscribe.
        warnings:
          type: array
          description: Non-blocking deliverability hints for template HTML (e.g. SVG image URLs)
          items:
            $ref: '#/components/schemas/TemplateLintWarning'
    TemplateVersion:
      type: object
      required: [id, template_id, version, format, subject, html, text, variables, created_at]
      properties:
        id:
          type: string
          format: uuid
        template_id:
          type: string
          format: uuid
        version:
          type: integer
        format:
          type: string
          enum: [html, blocks]
          description: Immutable format snapshot from publish time
        subject:
          type: string
        preheader:
          type: string
          nullable: true
        design:
          type: object
          nullable: true
          additionalProperties: true
        html:
          type: string
          nullable: true
        text:
          type: string
          nullable: true
        variables:
          type: array
          nullable: true
          items:
            type: string
        created_by_user_id:
          type: string
          format: uuid
          nullable: true
        label:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    Asset:
      type: object
      required: [id, key, url, content_type, bytes, created_at]
      properties:
        id:
          type: string
          format: uuid
        key:
          type: string
        url:
          type: string
          format: uri
        content_type:
          type: string
        bytes:
          type: integer
        width:
          type: integer
          nullable: true
        height:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
    TemplateLintWarning:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum:
            - svg_image
            - relative_image_url
            - data_uri_image
            - missing_alt
            - non_https_image
            - empty_href
            - raw_html_block
            - low_contrast
            - html_too_large
        message:
          type: string
        detail:
          type: string
          nullable: true
    TemplateResponse:
      type: object
      required: [template]
      properties:
        template:
          $ref: '#/components/schemas/Template'
    TemplateStarter:
      type: object
      required: [alias, name, description, subject, html, text, variables, unsubscribe_enabled]
      properties:
        alias:
          type: string
          example: password-reset
        name:
          type: string
        description:
          type: string
        subject:
          type: string
        preheader:
          type: string
          nullable: true
        html:
          type: string
        text:
          type: string
        design:
          type: object
          nullable: true
          additionalProperties: true
        variables:
          type: array
          items:
            type: string
        unsubscribe_enabled:
          type: boolean
    ListTemplatesResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Template'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateTemplateRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        subject:
          type: string
          description: Optional default subject line. Omit for layout-only templates used with per-send subjects.
        format:
          type: string
          enum: [html, blocks]
        preheader:
          type: string
        html:
          type: string
        text:
          type: string
        design:
          type: object
          additionalProperties: true
          description: >-
            Required when format is blocks (defaults to empty document). Supports
            `settings.globalCss` for author-supplied CSS compiled at render time.
        alias:
          type: string
        variables:
          type: array
          items:
            type: string
        unsubscribe_enabled:
          type: boolean
        category:
          type: string
          enum: [transactional, product, newsletter]
          default: transactional
    UpdateTemplateRequest:
      type: object
      properties:
        name:
          type: string
        subject:
          type: string
        format:
          type: string
          enum: [html, blocks]
        preheader:
          type: string
          nullable: true
        html:
          type: string
          nullable: true
        text:
          type: string
          nullable: true
        design:
          type: object
          nullable: true
          additionalProperties: true
        alias:
          type: string
          nullable: true
        variables:
          type: array
          nullable: true
          items:
            type: string
        unsubscribe_enabled:
          type: boolean
        category:
          type: string
          enum: [transactional, product, newsletter]
    TemplateActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [publish, duplicate]
        name:
          type: string
          description: Optional name when action is `duplicate`
    WebhookEndpoint:
      type: object
      required: [id, url, events, enabled, created_at, updated_at]
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ListWebhooksResponse:
      type: object
      required: [webhooks, has_more, next_cursor]
      properties:
        webhooks:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEndpoint'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateWebhookRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          description: Defaults to all supported events if omitted
    CreateWebhookResponse:
      type: object
      required: [webhook, secret]
      properties:
        webhook:
          $ref: '#/components/schemas/WebhookEndpoint'
        secret:
          type: string
          description: Signing secret (`whsec_…`), shown once at creation
    UpdateWebhookRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
        enabled:
          type: boolean
    WebhookResponse:
      type: object
      required: [webhook]
      properties:
        webhook:
          $ref: '#/components/schemas/WebhookEndpoint'
    EmailWebhookEventPayload:
      type: object
      required: [type, created_at, data]
      properties:
        type:
          $ref: '#/components/schemas/EmailWebhookEventType'
        created_at:
          type: string
          format: date-time
        data:
          type: object
          required: [email_id, to, from, subject, status]
          properties:
            email_id:
              type: string
            to:
              type: array
              items:
                type: string
                format: email
            from:
              type: string
            subject:
              type: string
            status:
              type: string
            bounce_reason:
              type: string
              nullable: true
            rcpt_to:
              type: string
              format: email
            cc:
              type: array
              items:
                type: string
                format: email
            message_id:
              type: string
              nullable: true
            in_reply_to:
              type: string
              nullable: true
            references:
              type: array
              items:
                type: string
            spam_status:
              type: string
              nullable: true
            attachment_count:
              type: integer
            attachments:
              type: array
              items:
                $ref: '#/components/schemas/ReceivedEmailAttachmentMeta'
            received_at:
              type: string
              format: date-time
    AutomationWebhookEventPayload:
      type: object
      required: [type, created_at, data]
      properties:
        type:
          type: string
          enum:
            - automation.started
            - automation.step_completed
            - automation.completed
            - automation.failed
        created_at:
          type: string
          format: date-time
        data:
          type: object
          required: [automation_id, automation_name, run_id, status, event_name]
          properties:
            automation_id:
              type: string
            automation_name:
              type: string
            run_id:
              type: string
            status:
              type: string
            event_name:
              type: string
            step_index:
              type: integer
              nullable: true
            step_type:
              type: string
              nullable: true
              enum: [delay, send_email, condition, wait_for_event]
            error:
              type: string
              nullable: true
    ContactWebhookEventPayload:
      type: object
      required: [type, created_at, data]
      properties:
        type:
          type: string
          enum: [contact.unsubscribed]
        created_at:
          type: string
          format: date-time
        data:
          type: object
          required: [email, source, scope]
          properties:
            email:
              type: string
              format: email
            source:
              type: string
              enum: [unsubscribe]
            scope:
              type: string
              description: >
                `all` adds a global suppression (blocks every send). `category` opts the
                address out of product or newsletter mail only; transactional sends still go through.
              enum: [all, category]
            category:
              type: string
              nullable: true
              enum: [product, newsletter]
              description: Present when scope is `category`
            category_label:
              type: string
              nullable: true
              description: Human-readable category label for UI copy
            suppression_id:
              type: string
              nullable: true
              description: Present when scope is `all`
            category_opt_out_id:
              type: string
              nullable: true
              description: Present when scope is `category`
            email_id:
              type: string
              nullable: true
              description: Public id of the email that carried the unsubscribe link, when known
    WebhookEventPayload:
      oneOf:
        - $ref: '#/components/schemas/EmailWebhookEventPayload'
        - $ref: '#/components/schemas/AutomationWebhookEventPayload'
        - $ref: '#/components/schemas/ContactWebhookEventPayload'
    EventRecord:
      type: object
      required: [id, name, user_id, email, data, source, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        user_id:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
          format: email
        data:
          type: object
          additionalProperties: true
        source:
          type: string
          enum: [api, dashboard]
        created_at:
          type: string
          format: date-time
    TriggerEventRequest:
      type: object
      properties:
        name:
          type: string
          description: "Canonical event name (alias: `type`)"
        type:
          type: string
          description: Alias for name
        user_id:
          type: string
        userId:
          type: string
          description: Alias for user_id
        email:
          type: string
          format: email
        data:
          type: object
          additionalProperties: true
      anyOf:
        - required: [name]
        - required: [type]
    TriggerEventResponse:
      type: object
      required: [event, matched_automations, resumed_runs, cancelled_runs]
      properties:
        event:
          $ref: '#/components/schemas/EventRecord'
        matched_automations:
          type: integer
        resumed_runs:
          type: integer
        cancelled_runs:
          type: integer
          description: Active runs cancelled because this event matched `trigger.cancel_on_events`
    AutomationTrigger:
      type: object
      required: [type, event]
      properties:
        type:
          type: string
          enum: [event]
        event:
          type: string
          example: user.created
        cancel_on_events:
          type: array
          items:
            type: string
          maxItems: 20
          description: When any listed event fires for the same recipient, cancel queued/running/waiting runs for this automation.
    AutomationStep:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [delay, send_email, send_sms, condition, wait_for_event]
      additionalProperties: true
      description: |
        Step object. Shape depends on `type`:
        - `delay`: `{ type, seconds }`
        - `send_email`: `{ type, email: { from, to, subject?, html?, text?, template? } }`
        - `send_sms`: `{ type, sms: { to, body, from?, messaging_service_sid? } }` (requires Twilio integration)
        - `condition`: `{ type, path, operator, value? }` (false stops the run)
        - `wait_for_event`: `{ type, event, timeout_seconds?, on_timeout? }` (`on_timeout` is `continue` or `fail`, default `fail`)
    Automation:
      type: object
      required: [id, name, status, trigger, steps, created_at, updated_at]
      properties:
        id:
          type: string
        name:
          type: string
        status:
          type: string
          enum: [draft, active, paused]
        trigger:
          $ref: '#/components/schemas/AutomationTrigger'
        steps:
          type: array
          items:
            $ref: '#/components/schemas/AutomationStep'
          minItems: 1
          maxItems: 20
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ListAutomationsResponse:
      type: object
      required: [automations]
      properties:
        automations:
          type: array
          items:
            $ref: '#/components/schemas/Automation'
    AutomationResponse:
      type: object
      required: [automation]
      properties:
        automation:
          $ref: '#/components/schemas/Automation'
    CreateAutomationRequest:
      type: object
      required: [name, trigger, steps]
      properties:
        name:
          type: string
        trigger:
          $ref: '#/components/schemas/AutomationTrigger'
        steps:
          type: array
          items:
            $ref: '#/components/schemas/AutomationStep'
          minItems: 1
          maxItems: 20
    UpdateAutomationRequest:
      type: object
      properties:
        name:
          type: string
        trigger:
          $ref: '#/components/schemas/AutomationTrigger'
        steps:
          type: array
          items:
            $ref: '#/components/schemas/AutomationStep'
          minItems: 1
          maxItems: 20
    AutomationActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [activate, pause]
    AutomationRunStep:
      type: object
      required:
        [id, step_index, step_type, status, output, error, started_at, completed_at, created_at, updated_at]
      properties:
        id:
          type: string
        step_index:
          type: integer
        step_type:
          type: string
          enum: [delay, send_email, condition, wait_for_event]
        status:
          type: string
          enum: [started, waiting, completed, failed]
        output:
          nullable: true
        error:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AutomationRun:
      type: object
      required:
        [
          id,
          automation_id,
          trigger_event_id,
          status,
          current_step,
          waiting_for_event,
          wait_timeout_at,
          context,
          last_error,
          cancel_requested_at,
          started_at,
          completed_at,
          created_at,
          updated_at,
        ]
      properties:
        id:
          type: string
        automation_id:
          type: string
        trigger_event_id:
          type: string
          nullable: true
        status:
          type: string
          enum: [queued, running, waiting, completed, failed, cancelled]
        current_step:
          type: integer
        waiting_for_event:
          type: string
          nullable: true
        wait_timeout_at:
          type: string
          format: date-time
          nullable: true
        context:
          type: object
          additionalProperties: true
        last_error:
          type: string
          nullable: true
        cancel_requested_at:
          type: string
          format: date-time
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        automation:
          $ref: '#/components/schemas/Automation'
        trigger_event:
          $ref: '#/components/schemas/EventRecord'
        steps:
          type: array
          items:
            $ref: '#/components/schemas/AutomationRunStep'
          description: Present on run detail responses
    ListAutomationRunsResponse:
      type: object
      required: [runs]
      properties:
        runs:
          type: array
          items:
            $ref: '#/components/schemas/AutomationRun'
    AutomationRunResponse:
      type: object
      required: [run]
      properties:
        run:
          $ref: '#/components/schemas/AutomationRun'
    AutomationRunActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [cancel, retry]
    Domain:
      type: object
      required:
        [id, name, status, created_at, verified_at, open_tracking, click_tracking, tls_mode, inbound_enabled, inbound_status, inbound_error]
      properties:
        id:
          type: string
        name:
          type: string
          example: example.com
        status:
          type: string
          enum: [pending, verified, failed]
        created_at:
          type: string
          format: date-time
        verified_at:
          type: string
          format: date-time
          nullable: true
        open_tracking:
          type: boolean
          description: Open tracking preference for this domain.
        click_tracking:
          type: boolean
          description: Click tracking preference for this domain.
        tls_mode:
          type: string
          enum: [opportunistic, enforced]
          description: Preferred outbound TLS behavior for this domain.
        inbound_enabled:
          type: boolean
          description: True when Ranla should accept inbound mail for this domain.
        inbound_status:
          type: string
          enum: [disabled, pending, active, error]
          description: '`pending` before verification, `active` once inbound receiving is configured, or `error` when setup fails.'
        inbound_error:
          type: string
          nullable: true
          description: Last inbound configuration error, if any.
    DnsRecord:
      type: object
      required: [type, host, value, purpose]
      properties:
        type:
          type: string
          example: TXT
          description: DNS record type (`TXT`, `CNAME`, or `MX`)
        host:
          type: string
        value:
          type: string
          description: Record content. For MX records, the mail server hostname (priority is in `priority`).
        priority:
          type: integer
          description: MX preference when `type` is `MX` (lower is preferred).
        purpose:
          type: string
          description: Human-readable purpose (`Domain ownership`, `DKIM`, `Return path`, `SPF`, `DMARC`, `Inbound receiving`)
    ListDomainsResponse:
      type: object
      required: [domains, has_more, next_cursor]
      properties:
        domains:
          type: array
          items:
            $ref: '#/components/schemas/Domain'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateDomainRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: example.com
        inbound_enabled:
          type: boolean
          description: Request inbound receiving for the domain as soon as verification succeeds.
        inboundEnabled:
          type: boolean
          description: Alias for inbound_enabled.
    CreateDomainResponse:
      type: object
      required: [domain, records, inbound_records, inbound_note]
      properties:
        domain:
          $ref: '#/components/schemas/Domain'
        records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
        inbound_records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
        inbound_note:
          type: string
        mail_server_dns_synced:
          type: boolean
          description: Whether mail-server DKIM/return-path records were synced into the response records
        mail_server_dns_error:
          type: string
          description: Present when mail-server DNS sync failed; ownership/SPF/DMARC records are still returned
    DomainDetailResponse:
      type: object
      required: [domain, records, analysis, inbound_records, inbound_note]
      properties:
        domain:
          $ref: '#/components/schemas/Domain'
        records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
        analysis:
          $ref: '#/components/schemas/DomainDnsAnalysis'
        inbound_records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
        inbound_note:
          type: string
        inbound_configured:
          type: boolean
        inbound_pending:
          type: boolean
        inbound_error:
          type: string
          nullable: true
        tracking_dns:
          $ref: '#/components/schemas/DomainTrackingDns'
        tracking_error:
          type: string
          nullable: true
          description: >-
            Present when tracking preferences were saved but delivery-side tracking
            provisioning failed. Customer-facing message; internal failure details are logged server-side.
        tls_error:
          type: string
          nullable: true
          description: >-
            Present when TLS mode was saved but delivery-side TLS provisioning failed.
            Customer-facing message; internal failure details are logged server-side.
    MailServerSigningStatus:
      type: object
      description: Mail-server delivery DNS statuses recorded when verify re-checks the domain.
      properties:
        dkim_ok:
          type: boolean
        return_path_ok:
          type: boolean
        dkim_status:
          type: string
          nullable: true
        return_path_status:
          type: string
          nullable: true
    DomainTrackingDns:
      type: object
      required: [required, host, target, ok]
      properties:
        required:
          type: boolean
          description: True when open or click tracking is enabled for this domain.
        host:
          type: string
          nullable: true
          description: Customer CNAME host (e.g. ps.example.com) when tracking is required.
        target:
          type: string
          nullable: true
          description: Tracking CNAME target when tracking is required.
        ok:
          type: boolean
          nullable: true
          description: Whether tracking DNS is live; null when tracking is disabled.
    VerifyDomainResponse:
      type: object
      required: [domain, records, verified, inbound_records, inbound_note]
      properties:
        domain:
          $ref: '#/components/schemas/Domain'
        records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
        analysis:
          $ref: '#/components/schemas/DomainDnsAnalysis'
        inbound_records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
        inbound_note:
          type: string
        verified:
          type: boolean
        auto:
          type: boolean
        checks:
          type: object
          properties:
            ownership:
              type: boolean
            spf:
              type: boolean
            dkim:
              type: boolean
            dmarc:
              type: boolean
            return_path:
              type: boolean
        inbound_configured:
          type: boolean
        inbound_pending:
          type: boolean
        inbound_error:
          type: string
          nullable: true
        signing_ready:
          type: boolean
          description: True when the mail server will DKIM-sign with this domain's key (not the shared pool fallback).
        mail_server_signing:
          $ref: '#/components/schemas/MailServerSigningStatus'
    VerifyDomainErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
        pending:
          type: boolean
          description: >
            True when public DNS is not fully ready yet (propagation). Clients should
            show a per-record checklist and keep retrying — this is not a hard failure.
        checks:
          type: object
          properties:
            ownership:
              type: boolean
            spf:
              type: boolean
            dkim:
              type: boolean
            dmarc:
              type: boolean
            return_path:
              type: boolean
        mail_server_signing:
          $ref: '#/components/schemas/MailServerSigningStatus'
        records:
          type: array
          items:
            $ref: '#/components/schemas/DnsRecord'
    RemoteApplyDnsCredentials:
      type: object
      properties:
        apiToken:
          type: string
          description: Inline Cloudflare API token instead of the stored dashboard integration
        zoneId:
          type: string
          description: Optional Cloudflare zone id when using an inline token
        cloudflareApiToken:
          type: string
          description: Alias for apiToken
        cloudflareZoneId:
          type: string
          description: Alias for zoneId
        apiKey:
          type: string
          description: GoDaddy API key
        apiSecret:
          type: string
          description: GoDaddy API secret
        godaddyApiKey:
          type: string
          description: Alias for apiKey
        godaddyApiSecret:
          type: string
          description: Alias for apiSecret
        vercelApiToken:
          type: string
          description: Alias for apiToken when provider is vercel
        vercelTeamId:
          type: string
          description: Optional Vercel team id for team-scoped DNS
        teamId:
          type: string
          description: Inline Vercel team id (alias for vercelTeamId)
    ApplyDomainDnsResponse:
      type: object
      required: [provider, domain, results]
      properties:
        provider:
          type: string
          enum: [cloudflare, godaddy, vercel]
        domain:
          type: string
        results:
          type: array
          items:
            $ref: '#/components/schemas/ApplyDnsRecordResult'
    ApplyDnsRecordResult:
      type: object
      required: [purpose, host, action]
      properties:
        purpose:
          type: string
        host:
          type: string
        action:
          type: string
          enum: [created, updated, unchanged, merged]
        detail:
          type: string
    UpdateDomainRequest:
      type: object
      description: |
        Tracking and TLS settings are persisted immediately in Ranla.
        Preferences are stored on the domain immediately when updated.
      properties:
        open_tracking:
          type: boolean
        openTracking:
          type: boolean
          description: Alias for open_tracking
        click_tracking:
          type: boolean
        clickTracking:
          type: boolean
          description: Alias for click_tracking
        tls_mode:
          type: string
          enum: [opportunistic, enforced]
        tlsMode:
          type: string
          enum: [opportunistic, enforced]
          description: Alias for tls_mode
        inbound_enabled:
          type: boolean
          description: Enable or disable inbound receiving for this domain.
        inboundEnabled:
          type: boolean
          description: Alias for inbound_enabled
    DomainDnsAnalysis:
      type: object
      required: [dmarc, bimi]
      properties:
        dmarc:
          $ref: '#/components/schemas/DomainDmarcAnalysis'
        bimi:
          $ref: '#/components/schemas/DomainBimiAnalysis'
    DomainDmarcAnalysis:
      type: object
      required: [host, configured, record, policy, rua, guidance]
      properties:
        host:
          type: string
        configured:
          type: boolean
        record:
          type: string
          nullable: true
        policy:
          type: string
          nullable: true
          description: Parsed DMARC `p=` policy when present (`none`, `quarantine`, or `reject`)
        rua:
          type: array
          items:
            type: string
        guidance:
          type: string
    DomainBimiAnalysis:
      type: object
      required:
        [host, configured, record, location, authority, requires_dmarc_enforcement, guidance]
      properties:
        host:
          type: string
        configured:
          type: boolean
        record:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
          description: Parsed BIMI `l=` logo URL when present
        authority:
          type: string
          nullable: true
          description: Parsed BIMI `a=` VMC URL when present
        requires_dmarc_enforcement:
          type: boolean
          description: True when BIMI is unlikely to work until DMARC is set to p=quarantine or p=reject
        guidance:
          type: string
    CloudflareIntegrationStatus:
      type: object
      required: [connected, hint, connectedAt]
      properties:
        connected:
          type: boolean
        hint:
          type: string
          nullable: true
          description: Last 4 characters of the stored token
        connectedAt:
          type: string
          format: date-time
          nullable: true
    CloudflareIntegrationResponse:
      type: object
      required: [cloudflare]
      properties:
        cloudflare:
          $ref: '#/components/schemas/CloudflareIntegrationStatus'
    VercelIntegrationStatus:
      type: object
      required: [connected, hint, connectedAt, teamId]
      properties:
        connected:
          type: boolean
        hint:
          type: string
          nullable: true
          description: Last 4 characters of the stored token
        connectedAt:
          type: string
          format: date-time
          nullable: true
        teamId:
          type: string
          nullable: true
          description: Optional Vercel team id for team-scoped DNS
    VercelIntegrationResponse:
      type: object
      required: [vercel]
      properties:
        vercel:
          $ref: '#/components/schemas/VercelIntegrationStatus'
    TwilioIntegrationStatus:
      type: object
      required: [connected, hint, connectedAt, defaultFrom, messagingServiceSid]
      properties:
        connected:
          type: boolean
        hint:
          type: string
          nullable: true
          description: Last 4 characters of the stored Account SID
        connectedAt:
          type: string
          format: date-time
          nullable: true
        defaultFrom:
          type: string
          nullable: true
          description: Default E.164 From number for send_sms steps
        messagingServiceSid:
          type: string
          nullable: true
          description: Default Twilio Messaging Service SID for send_sms steps
    TwilioConnectRequest:
      type: object
      required: [accountSid, authToken]
      properties:
        accountSid:
          type: string
          description: Twilio Account SID (AC…)
        authToken:
          type: string
          description: Twilio Auth Token
        defaultFrom:
          type: string
          description: Default E.164 From number (required if messagingServiceSid omitted)
        messagingServiceSid:
          type: string
          description: Default Messaging Service SID (required if defaultFrom omitted)
    TwilioIntegrationResponse:
      type: object
      required: [twilio]
      properties:
        twilio:
          $ref: '#/components/schemas/TwilioIntegrationStatus'
    ErrorCode:
      type: string
      description: Machine-readable error code
      enum:
        - validation_error
        - unauthorized
        - forbidden
        - not_found
        - conflict
        - plan_limit
        - domain_limit
        - idempotency_conflict
        - rate_limited
        - upstream_error
        - not_configured
        - internal_error
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [message]
          properties:
            message:
              type: string
            code:
              $ref: '#/components/schemas/ErrorCode'
            upgrade_url:
              type: string
              description: Dashboard path to upgrade when code is plan_limit or domain_limit
            details:
              description: >
                Optional structured details. For send blocks this may include
                `suppressed` (global suppression list) or `category_unsubscribed`
                plus `category` and `email_id` (category opt-out).
    ApiKey:
      type: object
      required: [id, name, scope, prefix, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        scope:
          type: string
          enum: [full, sending]
        prefix:
          type: string
          description: Visible key prefix (e.g. `rnl_live_ab`)
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true
    ListApiKeysResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateApiKeyRequest:
      type: object
      properties:
        name:
          type: string
        scope:
          type: string
          enum: [full, sending]
          default: full
    CreateApiKeyResponse:
      type: object
      required: [id, name, scope, prefix, token, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        scope:
          type: string
          enum: [full, sending]
        prefix:
          type: string
        token:
          type: string
          description: Full API key (`rnl_…`), shown once at creation
        created_at:
          type: string
          format: date-time
    SmtpCredential:
      type: object
      required:
        [id, name, password_prefix, username, host, port, last_used_at, revoked_at, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        password_prefix:
          type: string
          description: First characters of the SMTP password for identification
        username:
          type: string
          description: Always `supersendtx`
        host:
          type: string
          example: smtp.supersendtx.com
        port:
          type: integer
          example: 587
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    ListSmtpCredentialsResponse:
      type: object
      required: [data, has_more, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SmtpCredential'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    CreateSmtpCredentialRequest:
      type: object
      properties:
        name:
          type: string
    CreateSmtpCredentialResponse:
      allOf:
        - $ref: '#/components/schemas/SmtpCredential'
        - type: object
          required: [password]
          properties:
            password:
              type: string
              description: Full SMTP password (`stxsmtp_…`), shown once at creation
    SimpleErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
