openapi: 3.1.0
info:
  title: Washeej Public API v1 — WhatsApp Templates Lifecycle
  version: 1.0.0
  description: |
    Meta template lifecycle for external systems (Cloud accounts).
    Global feature gate FEATURE_DEVELOPER_TEMPLATE_LIFECYCLE_V1 remains false;
    canary via API credential/account assignment lists (no hardcoded account IDs).
    Dual-surface: the same lifecycle engine is exposed on Legacy External API
    under /external-api/templates* (including POST /external-api/templates/send).
    Public docs: /api-v1-documentation/reference/templates/lifecycle and
    /api-v1-documentation/openapi/templates.yaml.
  contact:
    name: Washeej Developer Platform
servers:
  - url: https://app.washeej.com/v1
    description: Production
security:
  - ClientCredentials: []
  - BearerAuth: []
tags:
  - name: TemplatesLifecycle
  - name: TemplatesSend
  - name: Accounts
  - name: Webhooks
paths:
  /whatsapp-accounts:
    get:
      tags: [Accounts]
      summary: List WhatsApp accounts
      description: >
        Dual-surface with GET /external-api/whatsapp-accounts.
        Omit page/per_page for the legacy full-list shape.
        Pass page and/or per_page for additive pagination (max per_page=100, default 25).
        Ordered by id DESC. Never returns access tokens.
      operationId: listWhatsappAccounts
      security:
        - ClientCredentials: [account:read]
      parameters:
        - name: page
          in: query
          required: false
          schema: { type: integer, minimum: 1 }
        - name: per_page
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
  /whatsapp-accounts/{id}/capabilities:
    get:
      tags: [Accounts]
      summary: Template capabilities for an account
      operationId: accountTemplateCapabilities
      parameters:
        - $ref: '#/components/parameters/AccountIdPath'
      responses:
        '200':
          description: Capabilities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CapabilitiesResponse'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /capabilities/templates:
    get:
      tags: [Accounts]
      summary: Template capabilities (query form)
      operationId: queryTemplateCapabilities
      parameters:
        - name: whatsapp_account_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Capabilities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CapabilitiesResponse'
  /templates/validate:
    post:
      tags: [TemplatesLifecycle]
      summary: Validate template locally (no Meta call)
      operationId: validateTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateWriteBody'
      responses:
        '200':
          description: Validation result (provider_calls=0)
        '422':
          $ref: '#/components/responses/Error'
  /templates:
    get:
      tags: [TemplatesLifecycle]
      summary: List templates
      operationId: listTemplates
      parameters:
        - name: whatsapp_account_id
          in: query
          schema: { type: integer }
        - name: status
          in: query
          schema: { type: string }
        - name: language
          in: query
          schema: { type: string }
        - name: category
          in: query
          schema: { type: string }
        - name: search
          in: query
          schema: { type: string }
        - name: sendable
          in: query
          schema: { type: boolean }
        - name: updated_after
          in: query
          schema: { type: string, format: date-time }
        - name: page
          in: query
          schema: { type: integer, minimum: 1 }
        - name: per_page
          in: query
          schema: { type: integer, maximum: 100 }
      responses:
        '200':
          description: Paginated templates
    post:
      tags: [TemplatesLifecycle]
      summary: Create and submit template to Meta
      operationId: createTemplate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateWriteBody'
      responses:
        '201':
          description: Submitted to Meta
        '409':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'
  /templates/sync:
    post:
      tags: [TemplatesLifecycle]
      summary: Sync all templates for a Cloud account
      operationId: syncAccountTemplates
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [whatsapp_account_id]
              properties:
                whatsapp_account_id: { type: integer }
      responses:
        '200':
          description: Sync truth
  /templates/{id}:
    get:
      tags: [TemplatesLifecycle]
      summary: Template detail by Washeej ID
      operationId: getTemplate
      parameters:
        - $ref: '#/components/parameters/TemplateIdPath'
      responses:
        '200':
          description: Template
        '404':
          $ref: '#/components/responses/Error'
    patch:
      tags: [TemplatesLifecycle]
      summary: Edit template (lifecycle rules)
      operationId: patchTemplate
      parameters:
        - $ref: '#/components/parameters/TemplateIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                template_version: { type: string }
                body: { type: string }
                header_text: { type: string }
                footer: { type: string }
                buttons: { type: array, items: { type: object } }
                category: { type: string }
      responses:
        '200':
          description: Updated
        '409':
          $ref: '#/components/responses/Error'
    delete:
      tags: [TemplatesLifecycle]
      summary: Delete template
      operationId: deleteTemplate
      parameters:
        - $ref: '#/components/parameters/TemplateIdPath'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Deletion state
        '422':
          $ref: '#/components/responses/Error'
  /templates/{id}/sync:
    post:
      tags: [TemplatesLifecycle]
      summary: Sync one template
      operationId: syncOneTemplate
      parameters:
        - $ref: '#/components/parameters/TemplateIdPath'
      responses:
        '200':
          description: Sync + template truth
  /templates/send:
    post:
      tags: [TemplatesSend]
      summary: Send approved Cloud template (dual-surface; durable Idempotency-Key)
      description: >
        Available on POST /v1/templates/send and POST /external-api/templates/send.
        Requires messages:send (or templates:send). Cloud + approved only when lifecycle gate open.
        Idempotency-Key is REQUIRED. Missing header → 422 idempotency_key_required (no provider call).
        Scope: tenant(user_id) + credential + operation send (stored ats:send:{credential_id}:{key});
        v1 and external-api share the namespace. Replay same key+payload; 409 on conflict;
        in-flight may return 409 idempotency_in_flight; provider_outcome_unknown never blind-resubmits.
        Legacy POST /external-api/inbox/send-template-message does not require this header.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      operationId: sendTemplate
      deprecated: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [template_id, mobile]
              properties:
                template_id: { type: integer }
                whatsapp_account_id: { type: integer }
                mobile_code: { type: string }
                mobile: { type: string }
                body_variables:
                  type: array
                  items: { type: string }
      responses:
        '201':
          description: Accepted by Meta (includes wamid when available)
        '422':
          description: template_not_approved / template_required / validation
components:
  securitySchemes:
    ClientCredentials:
      type: apiKey
      in: header
      name: client-id
      description: Pair with client-secret header. Scopes listed in config/api_scopes.php.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
  parameters:
    AccountIdPath:
      name: id
      in: path
      required: true
      schema: { type: integer }
    TemplateIdPath:
      name: id
      in: path
      required: true
      schema: { type: integer }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8 }
  responses:
    Error:
      description: Stable error envelope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  schemas:
    SuccessEnvelope:
      type: object
      required: [ok, data, meta]
      properties:
        ok: { type: boolean, const: true }
        data: { type: object }
        meta:
          type: object
          properties:
            request_id: { type: string }
    ErrorEnvelope:
      type: object
      required: [ok, error]
      properties:
        ok: { type: boolean, const: false }
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - validation_failed
                - unauthorized
                - insufficient_scope
                - feature_disabled
                - not_found
                - template_api_cloud_only
                - account_not_connected
                - provider_credentials_unavailable
                - template_already_exists
                - template_not_editable
                - template_not_deletable
                - template_not_approved
                - template_required
                - unsupported_component
                - missing_variable_example
                - stale_template_version
                - idempotency_conflict
                - idempotency_key_required
                - idempotency_key_invalid
                - idempotency_in_flight
                - provider_outcome_unknown
                - provider_confirmed_failure
                - provider_rejected
                - provider_validation_failed
                - provider_rate_limited
                - provider_unavailable
                - sync_failed
                - rate_limited
            message: { type: string }
            details: { type: object, additionalProperties: true }
            request_id: { type: string }
            retryable: { type: boolean }
            retry_after: { type: integer }
            provider_code: { type: string }
        meta:
          type: object
          properties:
            request_id: { type: string }
    CapabilitiesResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                account_id: { type: integer }
                channel_type: { type: string, enum: [cloud, device] }
                connection_state: { type: string }
                template_management_supported: { type: boolean }
                template_send_supported: { type: boolean }
                template_sync_supported: { type: boolean }
                supported_categories:
                  type: array
                  items: { type: string }
                supported_languages:
                  type: array
                  items: { type: string }
                write_enabled: { type: boolean }
                send_enabled: { type: boolean }
                blockers:
                  type: array
                  items: { type: string }
    TemplateWriteBody:
      type: object
      required: [whatsapp_account_id, name, language, category, body]
      properties:
        whatsapp_account_id: { type: integer }
        name:
          type: string
          pattern: '^[a-z0-9_]+$'
        language: { type: string }
        category: { type: string, enum: [MARKETING, UTILITY] }
        header_format: { type: string, enum: [NONE, TEXT] }
        header_text: { type: string }
        body: { type: string }
        body_examples:
          type: array
          items: { type: string }
        footer: { type: string }
        buttons:
          type: array
          items:
            type: object
            properties:
              type: { type: string, enum: [QUICK_REPLY, URL, PHONE_NUMBER] }
    WebhookEvent:
      type: object
      required: [event, event_id, occurred_at, api_version, data]
      properties:
        event:
          type: string
          enum:
            - template.created
            - template.submitted
            - template.status.updated
            - template.rejected
            - template.approved
            - template.deleted
            - template.sync.failed
        event_id: { type: string }
        occurred_at: { type: string, format: date-time }
        api_version: { type: string, const: v1 }
        data:
          type: object
          properties:
            template_id: { type: integer }
            whatsapp_account_id: { type: integer }
            name: { type: string }
            language: { type: string }
            previous_status: { type: string }
            canonical_status: { type: string }
            provider_status: { type: string }
            send_capability: { type: boolean }
            reason: { type: ['string', 'null'] }
            last_synced_at: { type: string, format: date-time }
x-washeej-webhook-headers:
  X-Washeej-Event: event name
  X-Washeej-Event-Id: event_id
  X-Washeej-Timestamp: unix or ISO timestamp
  X-Washeej-Signature: HMAC signature
x-washeej-deprecation-policy: |
  Existing POST /v1/templates/send remains supported.
  Additive lifecycle endpoints are canary-gated.
  No silent breaking changes to response envelopes of existing routes.
