# QNNECT Public API — canonical specification (English).
# ЕДИНСТВЕННЫЙ ИСТОЧНИК ИСТИНЫ для внешней документации API.
#
# Русская версия НЕ редактируется руками: docs/api/openapi.ru.yaml содержит только
# переводы описаний, а docs/api/build_docs.py собирает из них dist/openapi.ru.yaml
# и все производные артефакты (llms-full.*.md, reference.*.html).
# После ЛЮБОЙ правки этого файла или оверлея: .venv/bin/python docs/api/build_docs.py
# CI-гард: qnnect_small/test_api_docs.py (устаревший dist / осиротевшие ключи → красный).
#
# Покрытие: ТОЛЬКО поверхность api_token (channel token). Легаси-эндпоинты с
# партнёрским gs_token и неавторизованный OTP сюда не входят намеренно.
openapi: 3.1.0
info:
  title: QNNECT Public API
  version: "1.0"
  description: |
    HTTP API for sending WhatsApp messages and templates through your QNNECT
    channel, and for reading the channel's template catalog.

    ## Authentication

    Every request is authenticated with two values you can copy from the
    QNNECT cabinet (**Integrations → API**):

    * `channel_uuid` — the channel identifier, part of the URL path;
    * `api_token` — the channel's secret token, passed as the `token` query
      parameter.

    Both values are per-channel. Keep the token secret: anyone who has it can
    send messages at your expense. The token can be regenerated in the cabinet
    at any time (the old one stops working immediately).

    ## Delivery model

    Sending endpoints are asynchronous: a successful call returns `202` with a
    `message_uuid` — the message is queued and delivered in the background.
    Delivery progress is reported through webhook integrations (see Webhooks).

    ## Errors

    Errors are JSON objects `{"error": "<code>"}` with an optional
    human-readable `message`. HTTP status codes: `400` invalid input,
    `401` missing or invalid token, `403` sending suspended (company blocked
    or out of funds), `404` unknown channel or template.
servers:
  - url: https://qnnect.kz
    description: Production
tags:
  - name: Messages
    description: Send text and media messages to WhatsApp users.
  - name: Templates
    description: Send approved WhatsApp templates and inspect the template catalog.

paths:
  /api/v1/{channel_uuid}/message/:
    post:
      operationId: sendMessage
      tags: [Messages]
      summary: Send a text or media message
      description: |
        Sends a free-form message to a WhatsApp user. Free-form messages are
        deliverable only inside the 24-hour customer-service window opened by
        the user's last inbound message; outside the window use a template.

        `type` selects the payload kind. For `text` the `text` field is
        required. For media types (`image`, `video`, `audio`, `document`,
        `sticker`) the `media.url` field is required; `text` becomes the
        caption where WhatsApp supports it (`image`, `video`, `document`).

        Supported media formats and size limits:

        | type | MIME | max size |
        |---|---|---|
        | image | image/jpeg, image/png | 5 MB |
        | video | video/mp4, video/3gpp | 16 MB |
        | audio | audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg | 16 MB |
        | document | pdf, doc(x), xls(x), ppt(x), txt | 100 MB |
        | sticker | image/webp | 500 KB |

        The `media.url` must be a public HTTPS URL.
      security:
        - channelToken: []
      parameters:
        - $ref: "#/components/parameters/ChannelUuid"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendMessageRequest"
            examples:
              text:
                summary: Plain text
                value:
                  type: text
                  phone: "77778866697"
                  text: "Hello! Your order is ready."
              image:
                summary: Image with caption
                value:
                  type: image
                  phone: "77778866697"
                  text: "Our new arrivals"
                  media: { url: "https://example.com/promo.jpg" }
              document:
                summary: Document with filename
                value:
                  type: document
                  phone: "77778866697"
                  text: "Price list attached"
                  media:
                    url: "https://example.com/pricelist.pdf"
                    filename: "pricelist.pdf"
      responses:
        "202":
          description: Message accepted and queued for delivery.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueuedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/SendingSuspended"
        "404":
          $ref: "#/components/responses/ChannelNotFound"

  /api/v1/{channel_uuid}/template/:
    post:
      operationId: sendTemplate
      tags: [Templates]
      summary: Send a template message
      description: |
        Sends an approved WhatsApp template. Templates work outside the
        24-hour window and are the way to start a conversation.

        `values` fills the numbered body placeholders `{{1}}, {{2}}, …` in
        order; the count must match the template exactly (see
        `GET /api/v1/{channel_uuid}/templates/` → `variables.body`).
        `header_values` and `button_values` fill header/button placeholders
        when the template has them.

        Media templates (IMAGE/VIDEO/DOCUMENT header): pass a public
        `media_url` (with optional `media_mime_type`) or a pre-registered
        `media_id`. Carousel templates: `cards` replaces card images by index;
        `null` keeps the sample image from the template.
      security:
        - channelToken: []
      parameters:
        - $ref: "#/components/parameters/ChannelUuid"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendTemplateRequest"
            examples:
              basic:
                summary: Body variables only
                value:
                  phone: "77778866697"
                  template_name: "order_confirm"
                  values: ["Ivan", "12345"]
              media:
                summary: Media template
                value:
                  phone: "77778866697"
                  template_name: "promo_image"
                  values: ["Ivan"]
                  media_url: "https://example.com/promo.jpg"
                  media_mime_type: "image/jpeg"
              carousel:
                summary: Carousel with card image override
                value:
                  phone: "77778866697"
                  template_name: "products_carousel"
                  values: ["Ivan"]
                  cards:
                    - { media_url: "https://example.com/product1.jpg" }
                    - null
      responses:
        "202":
          description: Template accepted and queued for delivery.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QueuedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/SendingSuspended"
        "404":
          description: Channel or template not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                channel: { value: { error: channel_not_found } }
                template: { value: { error: template_not_found } }

  /api/v1/{channel_uuid}/templates/:
    get:
      operationId: listTemplates
      tags: [Templates]
      summary: List channel templates
      description: |
        Returns the channel's template catalog with statuses and variable
        counts. Use it to discover what can be sent and how many `values`
        each template expects before calling the send endpoint.

        All statuses are returned by default, including `PENDING` and
        `REJECTED` — only `APPROVED` templates can actually be sent.
      security:
        - channelToken: []
      parameters:
        - $ref: "#/components/parameters/ChannelUuid"
        - name: status
          in: query
          required: false
          description: Filter by exact status (e.g. APPROVED, PENDING, REJECTED).
          schema:
            type: string
            enum: [APPROVED, PENDING, REJECTED, DISABLED, PAUSED, FLAGGED, IN_APPEAL, LOCKED, LIMIT_EXCEEDED, FAILED]
        - name: category
          in: query
          required: false
          description: Filter by category.
          schema:
            type: string
            enum: [MARKETING, UTILITY, AUTHENTICATION]
        - name: type
          in: query
          required: false
          description: Filter by template type (TEXT, IMAGE, VIDEO, DOCUMENT, CAROUSEL, …).
          schema:
            type: string
      responses:
        "200":
          description: Template catalog of the channel.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplatesListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/ChannelNotFound"

webhooks:
  incoming_message:
    post:
      operationId: whIncomingMessage
      summary: Incoming message from a WhatsApp user
      description: |
        Sent to your configured webhook URL when a WhatsApp user writes to the
        channel. Webhook URLs are configured in the cabinet
        (**Integrations → Webhooks**) and can be enabled per event type.

        Delivery: `POST` with JSON body, `Content-Type: application/json;
        charset=utf-8`. Respond with any `2xx` within 12 seconds; failed
        deliveries are retried in the background.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Acknowledge receipt with any 2xx status.
  outgoing_message:
    post:
      operationId: whOutgoingMessage
      summary: Outgoing message sent from the channel
      description: |
        Sent when a message leaves the channel — from the QNNECT UI, the API
        or an integration. Same delivery contract as `incoming_message`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Acknowledge receipt with any 2xx status.
  message_status_update:
    post:
      operationId: whMessageStatusUpdate
      summary: Delivery status of an earlier message changed
      description: |
        Sent when WhatsApp reports a status change for a previously sent
        message (`sent` → `delivered` → `read`, or `failed`). Correlate by
        `message_uuid`. Same delivery contract as `incoming_message`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Acknowledge receipt with any 2xx status.

components:
  securitySchemes:
    channelToken:
      type: apiKey
      in: query
      name: token
      description: |
        Channel API token (64 hex characters). Copy it in the cabinet:
        **Integrations → API**. Regenerating the token invalidates the old one
        immediately.

  parameters:
    ChannelUuid:
      name: channel_uuid
      in: path
      required: true
      description: Channel identifier (UUID) from the cabinet, **Integrations → API**.
      schema:
        type: string
        format: uuid

  responses:
    BadRequest:
      description: Invalid input. `error` holds a machine-readable code.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Token is missing (`missing_token`) or wrong (`invalid_token`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    SendingSuspended:
      description: Sending is suspended — the company is blocked or out of funds (`company_inactive`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ChannelNotFound:
      description: No channel with this `channel_uuid` (`channel_not_found`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Machine-readable error code.
        message:
          type: string
          description: Optional human-readable explanation.

    QueuedResponse:
      type: object
      required: [status, message_uuid]
      properties:
        status:
          type: string
          const: queued
          description: The message is queued for asynchronous delivery.
        message_uuid:
          type: string
          format: uuid
          description: Message identifier; use it to correlate webhook status updates.
        timestamp:
          type: string
          format: date-time
          description: Server time when the message was accepted.

    SendMessageRequest:
      type: object
      required: [type, phone]
      properties:
        type:
          type: string
          enum: [text, image, video, audio, document, sticker]
          description: Message kind. `text` requires `text`; media kinds require `media.url`.
        phone:
          type: string
          description: Recipient phone in international format, digits only (e.g. 77778866697).
        text:
          type: string
          description: Message text for `type=text`; caption for image/video/document.
        media:
          type: object
          description: Media descriptor; required for media types.
          properties:
            url:
              type: string
              format: uri
              description: Public HTTPS URL of the media file.
            filename:
              type: string
              description: Display filename (documents only).

    SendTemplateRequest:
      type: object
      required: [phone, template_name]
      properties:
        phone:
          type: string
          description: Recipient phone in international format, digits only.
        template_name:
          type: string
          description: Template name exactly as in the catalog (`listTemplates`).
        values:
          type: array
          items: { type: string }
          description: Body placeholder values `{{1}}, {{2}}, …` in order; count must match `variables.body`.
        header_values:
          type: array
          items: { type: string }
          description: Header placeholder values, when the template header has variables.
        button_values:
          type: array
          items: { type: string }
          description: Button placeholder values (dynamic URL suffixes etc.).
        media_url:
          type: string
          format: uri
          description: Public media URL for templates with a media header.
        media_id:
          type: string
          description: Pre-registered media id — alternative to `media_url`.
        media_mime_type:
          type: string
          description: MIME type of `media_url` (e.g. image/jpeg).
        cards:
          type: array
          description: Carousel only — per-card image overrides; `null` keeps the sample image.
          items:
            oneOf:
              - type: "null"
              - type: object
                properties:
                  media_url:
                    type: string
                    format: uri

    TemplatesListResponse:
      type: object
      required: [channel_uuid, templates_count, templates]
      properties:
        channel_uuid:
          type: string
          description: Echo of the requested channel.
        templates_count:
          type: integer
          description: Number of templates after filters.
        templates:
          type: array
          items:
            $ref: "#/components/schemas/Template"

    Template:
      type: object
      properties:
        template_id:
          type: string
          description: Template identifier.
        name:
          type: string
          description: Template name — use it as `template_name` when sending.
        category:
          type: string
          enum: [MARKETING, UTILITY, AUTHENTICATION]
          description: WhatsApp pricing category.
        language:
          type: string
          description: Template language code (e.g. ru, en).
        type:
          type: string
          description: Template type (TEXT, IMAGE, VIDEO, DOCUMENT, CAROUSEL, …).
        status:
          type: string
          description: Moderation status; only APPROVED templates can be sent.
        status_reason:
          type: [string, "null"]
          description: Meta's rejection reason for REJECTED/FAILED templates.
        quality_score:
          type: [string, "null"]
          enum: [GREEN, YELLOW, RED, UNKNOWN, null]
          description: WhatsApp quality rating of the template.
        content:
          type: string
          description: Body text with numbered placeholders `{{1}}, {{2}}, …`.
        header:
          type: string
          description: Header text for TEXT-header templates.
        header_media_url:
          type: string
          description: Sample media URL for media-header templates.
        buttons:
          type: [array, "null"]
          description: Button definitions as stored in the template.
        cards:
          type: [array, "null"]
          description: Carousel card definitions (media, body, buttons).
        variables:
          type: object
          description: How many placeholder values each part expects when sending.
          properties:
            body:
              type: integer
              description: Required length of `values`.
            header:
              type: integer
              description: Required length of `header_values`.
            button:
              type: integer
              description: Required length of `button_values`.

    WebhookEvent:
      type: object
      required: [event, phone, channel_id, message_uuid, status, sender, timestamp]
      properties:
        event:
          type: string
          enum: [incoming_message, outgoing_message, message_status_update]
          description: Event type; matches the webhook name.
        phone:
          type: string
          description: WhatsApp user's phone number.
        channel_id:
          type: string
          description: Channel identifier (Gupshup app id) the event belongs to.
        text:
          type: string
          description: Message text (empty for non-text content).
        message_uuid:
          type: string
          format: uuid
          description: Message identifier; matches `QueuedResponse.message_uuid` for API sends.
        status:
          type: string
          description: Current message status (queued, sent, delivered, read, failed, …).
        sender:
          type: string
          enum: [CLIENT, COMPANY, AI]
          description: Who authored the message.
        timestamp:
          type: string
          format: date-time
          description: Event time (ISO 8601).
