openapi: 3.1.0
info:
  title: Koltrix API
  version: "2.0.0"
  summary: Email infrastructure & newsletter platform — transactional send, lists, subscribers, webhooks, suppressions.
  description: |
    The Koltrix REST API is HTTP+JSON with conventional REST semantics. All
    endpoints under `/api/v2` are public, authenticated with a Bearer API key,
    and rate-limited per-key. Idempotency is supported on `POST /emails` via the
    `Idempotency-Key` header.

    Base URL: `https://api.koltrix.com`
  contact:
    name: Koltrix support
    url: https://docs.koltrix.com
    email: support@koltrix.com
  license:
    name: Proprietary
servers:
  - url: https://api.koltrix.com
    description: Production
  - url: http://localhost:4000
    description: Local self-hosted

security:
  - bearerAuth: []

tags:
  - name: Identity
    description: Who am I? What can my key do?
  - name: Transactional email
    description: Send one-off emails programmatically.
  - name: Newsletter lists
    description: Audiences for broadcast email.
  - name: Subscribers
    description: People on your lists.
  - name: Suppressions
    description: Recipients globally blocked for your tenant.
  - name: Webhooks
    description: Real-time delivery events.

paths:
  /api/v2/me:
    get:
      tags: [Identity]
      summary: Get tenant + permissions for the calling key
      operationId: getMe
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenant: { type: string, example: "acme" }
                  permissions:
                    type: array
                    items: { type: string }
                    example: ["read", "send", "newsletter"]
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v2/emails:
    post:
      tags: [Transactional email]
      summary: Send a transactional email
      operationId: sendEmail
      description: |
        Queues a single email for delivery. Returns immediately; actual SMTP
        send happens asynchronously and writes to your message log.

        **Idempotency**: pass `Idempotency-Key` to make this safe to retry — we
        cache the response for 24 hours per (tenant, key) so duplicate requests
        return the original response without re-queuing.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string }
          description: Any unique string per logical send (e.g. UUID).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendEmailRequest" }
            examples:
              minimal:
                summary: Minimal text email
                value:
                  from: "you@yourdomain.com"
                  to: ["lead@example.com"]
                  subject: "Hello from Koltrix"
                  body_text: "It works."
              html:
                summary: HTML email with reply-to
                value:
                  from: "Acme Sales <sales@acme.com>"
                  to: ["lead@example.com"]
                  cc: ["sales-internal@acme.com"]
                  subject: "Welcome to Acme"
                  body_html: "<h1>Welcome</h1><p>Glad to have you.</p>"
                  body_text: "Welcome\n\nGlad to have you."
      responses:
        "200":
          description: Queued (or replayed from idempotency cache)
          headers:
            Idempotent-Replayed:
              schema: { type: string }
              description: Present (= "true") only when the response came from the idempotency cache.
          content:
            application/json:
              schema:
                type: object
                properties:
                  queued: { type: boolean }
                  id: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /api/v2/lists:
    get:
      tags: [Newsletter lists]
      summary: List all newsletter lists
      operationId: listLists
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/List" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Newsletter lists]
      summary: Create a newsletter list
      operationId: createList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: "Weekly product updates" }
                description: { type: string }
                from_name: { type: string, example: "Acme Team" }
                from_address: { type: string, format: email, example: "hello@yourdomain.com" }
                reply_to: { type: string, format: email }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/List" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v2/lists/{list_id}/subscribers:
    parameters:
      - in: path
        name: list_id
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Subscribers]
      summary: List subscribers in a list
      operationId: listSubscribers
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [active, unsubscribed, pending, bounced] }
        - in: query
          name: limit
          schema: { type: integer, default: 100, maximum: 500 }
        - in: query
          name: offset
          schema: { type: integer, default: 0 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Subscriber" }
                  total: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Subscribers]
      summary: Add a subscriber to a list
      operationId: addSubscriber
      description: |
        Idempotent — if the email already exists on the list, name and tags are
        updated. If the list has `require_double_opt_in = true`, the new
        subscriber starts in `pending` status until they click the confirmation
        link sent to them.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                name: { type: string }
                tags:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Subscriber" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v2/lists/{list_id}/subscribers/{subscriber_id}:
    parameters:
      - in: path
        name: list_id
        required: true
        schema: { type: string, format: uuid }
      - in: path
        name: subscriber_id
        required: true
        schema: { type: string, format: uuid }
    delete:
      tags: [Subscribers]
      summary: Remove a subscriber from a list
      operationId: removeSubscriber
      responses:
        "204":
          description: Removed
        "401": { $ref: "#/components/responses/Unauthorized" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: kx_xxxxx
      description: Use a Bearer token starting with `kx_`. Create one at Settings → API Keys.

  responses:
    Unauthorized:
      description: Missing, malformed, or revoked API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "missing Bearer token" }
    Forbidden:
      description: API key lacks the required permission.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "missing permission: send" }
    BadRequest:
      description: Request body is invalid or missing required fields.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "from, to, subject are required" }
    RateLimited:
      description: Too many requests.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "rate limit exceeded" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
      required: [error]

    SendEmailRequest:
      type: object
      required: [from, to, subject]
      properties:
        from:
          type: string
          example: "you@yourdomain.com"
          description: Must be an address you own on a verified domain.
        to:
          type: array
          items: { type: string, format: email }
          minItems: 1
        cc:
          type: array
          items: { type: string, format: email }
        bcc:
          type: array
          items: { type: string, format: email }
        subject: { type: string }
        body_html: { type: string }
        body_text: { type: string }

    List:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        from_name: { type: string }
        from_address: { type: string, format: email }
        reply_to: { type: string, format: email }
        subscriber_count: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Subscriber:
      type: object
      properties:
        id: { type: string, format: uuid }
        list_id: { type: string, format: uuid }
        email: { type: string, format: email }
        name: { type: string }
        status:
          type: string
          enum: [active, pending, unsubscribed, bounced]
        tags:
          type: array
          items: { type: string }
        unsubscribe_token: { type: string }
        created_at: { type: string, format: date-time }
