openapi: 3.0.3
info:
  title: Datagroup Messaging API
  version: "1.0.0"
  description: |
    Send WhatsApp messages from your own systems and check delivery status — the API behind
    Datagroup's WhatsApp automation. Authenticate with a per-tenant API key created in the
    [Datagroup portal](https://portal.datagroup.co.za) (Admin → API keys).

    **Phase 1 (current release):** send approved WhatsApp templates, list your templates and
    numbers, and query delivery status/history. Free-text replies and WhatsApp Flows are planned
    for a later phase.

    **Channels:** every request/response carries an explicit `channel` field. `whatsapp` is the
    only value this release accepts — `sms` is reserved for a future Clickatell-backed SMS channel
    and is rejected today with `unsupported_channel`, so integrating against `channel` now means no
    contract change when SMS ships.
  contact:
    name: Datagroup
    url: https://www.datagroup.co.za
    email: info@datagroup.co.za
servers:
  - url: https://datagroupwebfxapp-cyexbwg3csgkdcbq.southafricanorth-01.azurewebsites.net/api/public/v1
    description: Production
tags:
  - name: Messages
    description: Send templates and query delivery status.
  - name: Catalog
    description: What you can send, and from which number.
security:
  - ApiKeyAuth: []
paths:
  /messages:
    post:
      tags: [Messages]
      summary: Send a template message
      description: |
        Sends an approved WhatsApp template to a South African mobile number. Supply
        `bodyParameters` in order if the template has `{{1}}, {{2}}, ...` placeholders (see
        `GET /templates` for each template's expected count).

        Pass an `Idempotency-Key` header to make retries safe — resending the same key returns
        the original result instead of sending a second message. Reusing a key always replays the
        first result; it is not re-validated against a changed request body, so treat each key as
        single-use per logical send.
      operationId: sendMessage
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendMessageRequest"
            examples:
              template:
                summary: Send order_ready with two body parameters
                value:
                  channel: whatsapp
                  to: "+27821234567"
                  templateName: order_ready
                  language: en_US
                  bodyParameters: ["Thabo", "ORD-1029"]
      responses:
        "201":
          description: Accepted and sent to WhatsApp.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendMessageResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Template not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: >
            Unsupported channel, template not approved, a bodyParameters count mismatch, or the
            tenant has zero/multiple sendable numbers and phoneNumberId was required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "502":
          description: WhatsApp (Meta) rejected the send. The message is still recorded — the response includes `messageId` so you can look it up later.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ErrorResponse"
                  - type: object
                    properties:
                      messageId:
                        type: string
                        format: uuid
    get:
      tags: [Messages]
      summary: List messages (delivery reporting)
      description: >
        Keyset-paginated list of your outbound messages, newest first. Defaults to the last 30
        days if `from`/`to` are omitted.
      operationId: listMessages
      parameters:
        - name: channel
          in: query
          schema: { type: string, enum: [whatsapp] }
        - name: status
          in: query
          schema: { type: string, enum: [Queued, Sent, Delivered, Read, Failed] }
        - name: templateName
          in: query
          schema: { type: string }
        - name: phoneNumberId
          in: query
          schema: { type: string, format: uuid }
        - name: from
          in: query
          description: ISO 8601 datetime; defaults to 30 days ago.
          schema: { type: string, format: date-time }
        - name: to
          in: query
          description: ISO 8601 datetime; defaults to now.
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
        - name: cursor
          in: query
          description: Opaque token from a prior page's `nextCursor`. Omit for the first page.
          schema: { type: string }
      responses:
        "200":
          description: A page of messages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          description: Unsupported channel.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimited"
  /messages/{messageId}:
    get:
      tags: [Messages]
      summary: Get one message's delivery status
      operationId: getMessage
      parameters:
        - name: messageId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: The message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Not found (or belongs to another tenant — never distinguished from "not found").
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimited"
  /templates:
    get:
      tags: [Catalog]
      summary: List approved templates you can send
      description: Includes the derived `bodyParameterCount` so you know how many `bodyParameters` a send needs.
      operationId: listTemplates
      responses:
        "200":
          description: Approved templates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Template" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /phone-numbers:
    get:
      tags: [Catalog]
      summary: List your active WhatsApp numbers
      description: >
        Only relevant when you have more than one number — with exactly one, `POST /messages` uses
        it automatically and `phoneNumberId` can be omitted.
      operationId: listPhoneNumbers
      responses:
        "200":
          description: Active numbers.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/PhoneNumber" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: >
        Your API key, created in the portal's API keys tab, e.g. `Authorization: Bearer dgwa_...`.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: An opaque string unique to this logical send. Retrying with the same key replays the original result.
      schema: { type: string }
  responses:
    BadRequest:
      description: Malformed request (invalid phone number, missing required field).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Unauthorized:
      description: Missing, unrecognised, revoked, or expired API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    RateLimited:
      description: Too many requests for this key. See the `Retry-After` header (seconds).
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    InternalError:
      description: Something went wrong on our side (e.g. the template's header image couldn't be resolved).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
  schemas:
    SendMessageRequest:
      type: object
      required: [to, templateName]
      properties:
        channel:
          type: string
          enum: [whatsapp]
          default: whatsapp
          description: Only `whatsapp` is available in this release; `sms` is reserved for a future phase.
        to:
          type: string
          description: South African mobile number, `+27...` or `0...`.
          example: "+27821234567"
        templateName:
          type: string
          example: order_ready
        language:
          type: string
          description: Meta language code. Defaults to the template's own language if omitted.
          example: en_US
        phoneNumberId:
          type: string
          format: uuid
          description: Required only if you have more than one active WhatsApp number (see `GET /phone-numbers`).
        bodyParameters:
          type: array
          items: { type: string }
          description: Positional values for the template's `{{1}}, {{2}}, ...` placeholders, in order. Omit or leave empty for a variable-free template.
        headerImageUrl:
          type: string
          description: Overrides the template's stored header image, if any.
    SendMessageResponse:
      type: object
      properties:
        messageId: { type: string, format: uuid }
        metaMessageId: { type: string, nullable: true }
        status: { type: string, example: Sent }
        conversationId: { type: string, format: uuid }
        channel: { type: string, example: whatsapp }
    MessageStatusResponse:
      type: object
      properties:
        messageId: { type: string, format: uuid }
        to: { type: string, nullable: true }
        direction: { type: string, example: Outbound }
        templateName: { type: string, nullable: true }
        status: { type: string, enum: [Queued, Sent, Delivered, Read, Failed] }
        timestamp: { type: string, format: date-time }
        errorDetail: { type: string, nullable: true }
        conversationId: { type: string, format: uuid }
        channel: { type: string, example: whatsapp }
    MessageListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/MessageStatusResponse" }
        nextCursor:
          type: string
          nullable: true
          description: Pass back as `cursor` for the next page; null when there are no more.
    Template:
      type: object
      properties:
        name: { type: string }
        language: { type: string }
        category: { type: string, nullable: true, example: UTILITY }
        bodyParameterCount: { type: integer }
        hasHeaderImage: { type: boolean }
    PhoneNumber:
      type: object
      properties:
        id: { type: string, format: uuid }
        displayPhoneNumber: { type: string, nullable: true }
        isDefault:
          type: boolean
          description: True only when this is your sole active number.
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invalid_api_key
                - key_revoked
                - key_expired
                - rate_limited
                - unsupported_channel
                - invalid_phone_number
                - invalid_request
                - template_not_found
                - template_not_approved
                - template_parameter_mismatch
                - ambiguous_phone_number
                - no_sendable_number
                - message_not_found
                - idempotency_conflict
                - whatsapp_send_failed
                - header_image_unavailable
                - internal_error
            message: { type: string }
            requestId:
              type: string
              format: uuid
              description: Quote this if you contact support about a specific call.
