# InfiQ WhatsApp Business API — OpenAPI 3.1.
# STATUS: DRAFT (x-status below). This file documents the CONTRACT the
# marketing site claims (see /developers); the platform team owns keeping it in
# lock-step with the live product. Served raw at
# https://www.infiq.in/openapi.yaml (linked from llms.txt).
#
# /messages is a MERO passthrough of Meta's WhatsApp Cloud API: send the exact
# body Meta's POST /{phone-number-id}/messages accepts, and InfiQ returns Meta's
# response verbatim plus a top-level `InfiQ_traceid` (128-hex) for support/log
# correlation. Meta message-format reference:
# https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages
openapi: 3.1.0
info:
  title: InfiQ WhatsApp Business API
  version: 0.1.0
  x-status: draft # ⇒ /developers/api still carries the draft posture
  summary: Send any WhatsApp message type (Meta Cloud API passthrough) and receive signed delivery events.
  description: |
    REST over HTTPS with JSON bodies. `POST /messages` accepts the **exact same
    request body as Meta's WhatsApp Cloud API** `POST /{phone-number-id}/messages`
    — text, template, media, interactive, location, contacts, reaction, and
    read/typing receipts. InfiQ forwards it to Meta and returns **Meta's response
    unchanged, plus a top-level `InfiQ_traceid`** (a 128-character hex id) so you
    can correlate any send with InfiQ's logs and support.

    **Draft notice** — this specification is a skeleton (`x-status: draft`).
    Paths, schemas and error codes shown here are the committed contract shape;
    field-level details may change before v1.0.0 is frozen. The live dashboard
    remains the source of truth for keys and limits.

    - Authentication: scoped API key in the `X-INFIQ-API-KEY` header
      (keys look like `isk_live_…`; sandbox keys `isk_test_…`).
    - Idempotency: send `Idempotency-Key` on writes; retries with the same
      key return the original result instead of double-sending.
    - Pagination: cursor-based on list endpoints (`cursor` + `limit`).
    - Rate limits: per-key; every response carries limit/remaining/reset
      headers so clients can self-throttle.
    - Every response (success **and** error) carries `InfiQ_traceid`.
  contact:
    name: InfiQ developer support
    url: https://www.infiq.in/developers
    email: support@infiq.in
  license:
    name: Proprietary
    identifier: LicenseRef-InfiQ-Proprietary
servers:
  # PLACEHOLDER ORIGIN — confirm with the platform team before v1.0.0.
  - url: https://api.infiq.in/v1
    description: Production (origin to be confirmed; base path /v1 is committed)
security:
  - ApiKeyAuth: []
tags:
  - name: Messages
    description: Send any WhatsApp message type (Meta Cloud API passthrough) and track its lifecycle.
  - name: Templates
    description: List WhatsApp templates and their Meta review status. (Coming soon.)
  - name: Webhooks
    description: |
      Register endpoints in the dashboard and subscribe to any of the 27
      cataloged events across 7 domains — `messages` (7), `campaigns` (4),
      `contacts` (3), `templates` (3), `apiKeys` (3), `waba` (3) and
      `system` (4). Every delivery shares one JSON envelope
      (`$ref: WebhookEvent`) with the per-event payload in `data`. Failed
      deliveries retry with exponential backoff for 24 hours until your
      endpoint returns 2xx.

      Two cataloged events — `contact.deleted` and `billing.threshold` — are
      subscribable and schema-published but have **no current producer**;
      their entries below are documented from the schema, not from a real
      observed delivery, and are flagged accordingly.

      **Headers on every delivery:**

      | Header | Carries | Notes |
      |---|---|---|
      | `X-InfiQ-Signature` | `t=<unix_seconds>,v1=<hex>` | `kid=<version>` appears after a secret rotation; `v0=<hex>` is added during the rotation grace window. |
      | `X-InfiQ-Timestamp` | Unix seconds at sign time | Matches the `t=` value above. |
      | `X-InfiQ-Event` | The **event id** (not the event type, despite the name) | Same value as `X-InfiQ-Event-Id`; read the event type from the envelope's `type` field instead. |
      | `X-InfiQ-Event-Id` | The event id (ULID) | Stable across retries — dedupe on this. |
      | `X-InfiQ-Delivery` | This delivery attempt's id | Changes on internal replay, not on ordinary retries. |
      | `X-InfiQ-Attempt` | Retry counter, 1-based | The envelope body's `_meta.attempt` is not reliable for this — see below. |
      | `X-InfiQ-Tenant` | Your tenant id | |
      | `X-InfiQ-Api-Version` | Envelope schema version, e.g. `2026-04` | |
      | `X-InfiQ-Live` | `true`/`false` | Whether this endpoint is in live or sandbox mode. |
      | `Idempotency-Key` | Same value as the event id | Stripe-style dedup alias. |
      | `Traceparent` | W3C Trace Context | Only present when `correlation.trace_id` is set. |

      **Verifying `X-InfiQ-Signature`** — the signed string is
      `{timestamp}.{raw_body}` (HMAC-SHA256), where `raw_body` is the exact
      bytes received — never re-serialize the JSON before verifying. Reject
      timestamps more than 120 seconds old, and always compare with a
      constant-time function. The signing key is derived from your
      endpoint's secret (shown once at creation, `whs_live_…`); see the
      dashboard's endpoint detail page for the current verification recipe.

      **A body quirk to know about:** `_meta.attempt` inside the envelope is
      hard-coded to `1` on every delivery, including retries — the true
      attempt number only lives in the `X-InfiQ-Attempt` header.
paths:
  /messages:
    post:
      operationId: sendMessage
      tags: [Messages]
      summary: Send a WhatsApp message (Meta Cloud API passthrough)
      description: >
        Send one message to one recipient using the **exact body Meta's Cloud
        API accepts**. InfiQ validates the API key, forwards to Meta, and
        returns Meta's response verbatim with an added top-level
        `InfiQ_traceid`. Delivery is asynchronous — the response confirms Meta
        accepted the message; delivery/read events arrive on your webhooks.
        Pick a payload from the examples dropdown (text, template, media,
        interactive, location, contacts, reaction, receipts).
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: Unique key per logical send; retries replay the original response.
          schema: { type: string, maxLength: 255 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendMessageRequest" }
            examples:
              text_plain:
                summary: Text — plain
                value:
                  messaging_product: whatsapp
                  recipient_type: individual
                  to: "919820000001"
                  type: text
                  text: { preview_url: false, body: "Hi Ananya! Your order INF-3091 is confirmed. 🎉" }
              text_link_preview:
                summary: Text — with URL preview
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: text
                  text: { preview_url: true, body: "Track your parcel: https://www.infiq.in/track/INF-3091" }
              text_reply_context:
                summary: Text — reply (quotes an inbound message)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  context: { message_id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAEhgUM0E0Qjc4RjcwQTE4RjNBQjEyMwA=" }
                  type: text
                  text: { body: "Yes — dispatching today." }
              template_hello_world:
                summary: Template — no variables
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template: { name: hello_world, language: { code: en_US } }
              template_body_params:
                summary: Template — positional body variables
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: order_confirmation
                    language: { code: en }
                    components:
                      - type: body
                        parameters:
                          - { type: text, text: Ananya }
                          - { type: text, text: INF-3091 }
              template_named_params:
                summary: Template — named body variables
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: order_confirmation_named
                    language: { code: en }
                    components:
                      - type: body
                        parameters:
                          - { type: text, parameter_name: customer_name, text: Ananya }
                          - { type: text, parameter_name: order_id, text: INF-3091 }
              template_currency_datetime:
                summary: Template — currency + date_time params
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: payment_receipt
                    language: { code: en }
                    components:
                      - type: body
                        parameters:
                          - type: currency
                            currency: { fallback_value: "₹1,499.00", code: INR, amount_1000: 1499000 }
                          - type: date_time
                            date_time: { fallback_value: "16 July 2026" }
              template_header_text:
                summary: Template — text header variable
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: appointment_reminder
                    language: { code: en }
                    components:
                      - type: header
                        parameters: [{ type: text, text: "Dr. Mehta" }]
                      - type: body
                        parameters: [{ type: text, text: "10:30 AM, 18 Jul" }]
              template_header_image:
                summary: Template — image header
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: shipment_update
                    language: { code: en }
                    components:
                      - type: header
                        parameters: [{ type: image, image: { link: "https://www.infiq.in/media/box.jpg" } }]
                      - type: body
                        parameters: [{ type: text, text: INF-3091 }]
              template_header_video:
                summary: Template — video header
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: product_launch
                    language: { code: en }
                    components:
                      - type: header
                        parameters: [{ type: video, video: { link: "https://www.infiq.in/media/teaser.mp4" } }]
              template_header_document:
                summary: Template — document header (invoice PDF)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: invoice_ready
                    language: { code: en }
                    components:
                      - type: header
                        parameters:
                          - type: document
                            document: { link: "https://www.infiq.in/media/INF-3091.pdf", filename: "Invoice-INF-3091.pdf" }
              template_header_location:
                summary: Template — location header
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: store_pickup
                    language: { code: en }
                    components:
                      - type: header
                        parameters:
                          - type: location
                            location: { latitude: "23.2599", longitude: "77.4126", name: "InfiQ Store", address: "MP Nagar, Bhopal" }
              template_quick_reply_buttons:
                summary: Template — quick-reply buttons
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: reorder_prompt
                    language: { code: en }
                    components:
                      - type: body
                        parameters: [{ type: text, text: Ananya }]
                      - type: button
                        sub_type: quick_reply
                        index: "0"
                        parameters: [{ type: payload, payload: REORDER_INF3091 }]
                      - type: button
                        sub_type: quick_reply
                        index: "1"
                        parameters: [{ type: payload, payload: STOP_PROMOS }]
              template_url_button:
                summary: Template — dynamic URL button
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: track_order
                    language: { code: en }
                    components:
                      - type: button
                        sub_type: url
                        index: "0"
                        parameters: [{ type: text, text: "INF-3091" }]
              template_copy_code_coupon:
                summary: Template — copy-code (coupon) button
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: festive_offer
                    language: { code: en }
                    components:
                      - type: button
                        sub_type: copy_code
                        index: "0"
                        parameters: [{ type: coupon_code, coupon_code: DIWALI20 }]
              template_auth_otp:
                summary: Template — authentication OTP (one-tap)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: login_verification
                    language: { code: en }
                    components:
                      - type: body
                        parameters: [{ type: text, text: "482913" }]
                      - type: button
                        sub_type: url
                        index: "0"
                        parameters: [{ type: text, text: "482913" }]
              template_carousel:
                summary: Template — carousel (2 cards)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: catalog_carousel
                    language: { code: en }
                    components:
                      - type: body
                        parameters: [{ type: text, text: Ananya }]
                      - type: carousel
                        cards:
                          - card_index: 0
                            components:
                              - type: header
                                parameters: [{ type: image, image: { link: "https://www.infiq.in/media/p1.jpg" } }]
                              - type: body
                                parameters: [{ type: text, text: "Kurta — ₹1,299" }]
                              - type: button
                                sub_type: quick_reply
                                index: "0"
                                parameters: [{ type: payload, payload: BUY_P1 }]
                          - card_index: 1
                            components:
                              - type: header
                                parameters: [{ type: image, image: { link: "https://www.infiq.in/media/p2.jpg" } }]
                              - type: body
                                parameters: [{ type: text, text: "Saree — ₹2,499" }]
                              - type: button
                                sub_type: quick_reply
                                index: "0"
                                parameters: [{ type: payload, payload: BUY_P2 }]
              template_limited_time_offer:
                summary: Template — limited-time offer + coupon
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: flash_sale
                    language: { code: en }
                    components:
                      - type: limited_time_offer
                        parameters:
                          - type: limited_time_offer
                            limited_time_offer: { expiration_time_ms: 1784908800000 }
                      - type: button
                        sub_type: copy_code
                        index: "0"
                        parameters: [{ type: coupon_code, coupon_code: FLASH50 }]
              template_flow_button:
                summary: Template — Flow button
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: template
                  template:
                    name: feedback_flow
                    language: { code: en }
                    components:
                      - type: button
                        sub_type: flow
                        index: "0"
                        parameters:
                          - type: action
                            action: { flow_token: "FT-9a3c", flow_action_data: { order_id: INF-3091 } }
              image_by_id:
                summary: Media — image by uploaded media id
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: image
                  image: { id: "1013859600286294" }
              image_by_link:
                summary: Media — image by link + caption
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: image
                  image: { link: "https://www.infiq.in/media/receipt.png", caption: "Your receipt for INF-3091" }
              video_by_id:
                summary: Media — video by media id
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: video
                  video: { id: "749321058812345" }
              video_by_link:
                summary: Media — video by link + caption
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: video
                  video: { link: "https://www.infiq.in/media/unboxing.mp4", caption: "How to set up your device" }
              audio_by_id:
                summary: Media — audio by media id
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: audio
                  audio: { id: "620198734501234" }
              audio_by_link:
                summary: Media — audio by link
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: audio
                  audio: { link: "https://www.infiq.in/media/voice-note.mp3" }
              document_by_id:
                summary: Media — document by media id
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: document
                  document: { id: "830912745601234", filename: "Statement-Jul2026.pdf" }
              document_by_link:
                summary: Media — document by link + filename + caption
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: document
                  document: { link: "https://www.infiq.in/media/INF-3091.pdf", filename: "Invoice-INF-3091.pdf", caption: "Invoice for your order" }
              sticker_by_id:
                summary: Media — sticker by media id
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: sticker
                  sticker: { id: "560913274500987" }
              sticker_by_link:
                summary: Media — sticker by link (.webp)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: sticker
                  sticker: { link: "https://www.infiq.in/media/thanks.webp" }
              location:
                summary: Location
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: location
                  location: { latitude: "23.2599", longitude: "77.4126", name: "InfiQ HQ", address: "MP Nagar, Bhopal 462011" }
              contacts:
                summary: Contacts card
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: contacts
                  contacts:
                    - name: { formatted_name: "InfiQ Support", first_name: InfiQ, last_name: Support }
                      phones: [{ phone: "+911140000000", type: WORK, wa_id: "911140000000" }]
                      emails: [{ email: "support@infiq.in", type: WORK }]
                      org: { company: "AIO Infinity Private Limited", title: "Support" }
              interactive_reply_buttons:
                summary: Interactive — reply buttons (max 3)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: button
                    body: { text: "Confirm your appointment for 18 Jul, 10:30 AM?" }
                    action:
                      buttons:
                        - { type: reply, reply: { id: confirm_1030, title: "Confirm" } }
                        - { type: reply, reply: { id: reschedule, title: "Reschedule" } }
                        - { type: reply, reply: { id: cancel, title: "Cancel" } }
              interactive_list:
                summary: Interactive — list menu
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: list
                    header: { type: text, text: "Today's menu" }
                    body: { text: "Pick a dish to order" }
                    footer: { text: "InfiQ Kitchen" }
                    action:
                      button: "View options"
                      sections:
                        - title: "Mains"
                          rows:
                            - { id: paneer, title: "Paneer Tikka", description: "₹249" }
                            - { id: biryani, title: "Veg Biryani", description: "₹199" }
                        - title: "Drinks"
                          rows:
                            - { id: lassi, title: "Sweet Lassi", description: "₹79" }
              interactive_cta_url:
                summary: Interactive — CTA URL button
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: cta_url
                    body: { text: "Your order has shipped." }
                    action:
                      name: cta_url
                      parameters: { display_text: "Track order", url: "https://www.infiq.in/track/INF-3091" }
              interactive_flow:
                summary: Interactive — Flow (navigate)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: flow
                    header: { type: text, text: "Book a slot" }
                    body: { text: "Tap below to choose a time." }
                    footer: { text: "Takes ~30 seconds" }
                    action:
                      name: flow
                      parameters:
                        flow_message_version: "3"
                        flow_token: "FT-9a3c"
                        flow_id: "1357924680"
                        flow_cta: "Book now"
                        flow_action: navigate
                        flow_action_payload: { screen: WELCOME, data: { order_id: INF-3091 } }
              interactive_location_request:
                summary: Interactive — location request
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: location_request_message
                    body: { text: "Share your delivery location to continue." }
                    action: { name: send_location }
              interactive_address_message:
                summary: Interactive — address collection (India)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: address_message
                    body: { text: "Where should we deliver INF-3091?" }
                    action:
                      name: address_message
                      parameters: { country: IN }
              interactive_single_product:
                summary: Interactive — single catalog product
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: product
                    body: { text: "Back in stock:" }
                    footer: { text: "Free shipping over ₹999" }
                    action: { catalog_id: "1234567890", product_retailer_id: SKU-KURTA-M }
              interactive_product_list:
                summary: Interactive — multi-product message
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: interactive
                  interactive:
                    type: product_list
                    header: { type: text, text: "New arrivals" }
                    body: { text: "Handpicked for you" }
                    footer: { text: "Tap to add to cart" }
                    action:
                      catalog_id: "1234567890"
                      sections:
                        - title: "Ethnic"
                          product_items:
                            - { product_retailer_id: SKU-KURTA-M }
                            - { product_retailer_id: SKU-SAREE-RED }
              reaction:
                summary: Reaction — emoji on an inbound message
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: reaction
                  reaction: { message_id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAEhgUM0E0Qjc4RjcwQTE4RjNBQjEyMwA=", emoji: "👍" }
              reaction_remove:
                summary: Reaction — remove (empty emoji)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  type: reaction
                  reaction: { message_id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAEhgUM0E0Qjc4RjcwQTE4RjNBQjEyMwA=", emoji: "" }
              image_reply_context:
                summary: Media — image as a reply (context)
                value:
                  messaging_product: whatsapp
                  to: "919820000001"
                  context: { message_id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAEhgUM0E0Qjc4RjcwQTE4RjNBQjEyMwA=" }
                  type: image
                  image: { link: "https://www.infiq.in/media/receipt.png", caption: "Here's your receipt" }
              mark_as_read:
                summary: Receipt — mark inbound message as read
                value:
                  messaging_product: whatsapp
                  status: read
                  message_id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAEhgUM0E0Qjc4RjcwQTE4RjNBQjEyMwA="
              typing_indicator:
                summary: Receipt — typing indicator (with read)
                value:
                  messaging_product: whatsapp
                  status: read
                  message_id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAEhgUM0E0Qjc4RjcwQTE4RjNBQjEyMwA="
                  typing_indicator: { type: text }
      responses:
        "200":
          description: Meta accepted the message (or the receipt). Body is Meta's response verbatim + `InfiQ_traceid`.
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SendMessageResponse" }
              examples:
                message_accepted:
                  summary: Message accepted by Meta
                  value:
                    messaging_product: whatsapp
                    contacts: [{ input: "919820000001", wa_id: "919820000001" }]
                    messages: [{ id: "wamid.HBgLOTE5ODIwMDAwMDAxFQIAERgSN0YxN0QxRUQ2M0E0M0FBOEQwAA==", message_status: accepted }]
                    InfiQ_traceid: "a1b2c3d4e5f60718293a4b5c6d7e8f900f1e2d3c4b5a69788796a5b4c3d2e1f0112233445566778899aabbccddeeff000123456789abcdeffedcba9876543210"
                receipt_ack:
                  summary: Read / typing receipt acknowledged
                  value:
                    success: true
                    InfiQ_traceid: "112233445566778899aabbccddeeff000123456789abcdeffedcba9876543210a1b2c3d4e5f60718293a4b5c6d7e8f900f1e2d3c4b5a69788796a5b4c3d2e1f0"
        "400":
          description: Meta rejected the request. Body is Meta's error object + `InfiQ_traceid`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaErrorResponse" }
              examples:
                invalid_parameter:
                  summary: "100 — invalid parameter"
                  value:
                    error:
                      message: "(#100) Invalid parameter"
                      type: OAuthException
                      code: 100
                      error_data: { messaging_product: whatsapp, details: "Param 'to' is not a valid WhatsApp number." }
                      error_subcode: 2494010
                      fbtrace_id: A1bCd2EfGh3IjKl
                    InfiQ_traceid: "0f1e2d3c4b5a69788796a5b4c3d2e1f0a1b2c3d4e5f60718293a4b5c6d7e8f90fedcba9876543210112233445566778899aabbccddeeff000123456789abcdef"
                recipient_not_in_allowed_list:
                  summary: "131030 — recipient not in allowed list (sandbox)"
                  value:
                    error:
                      message: "(#131030) Recipient phone number not in allowed list"
                      type: OAuthException
                      code: 131030
                      error_data: { messaging_product: whatsapp, details: "Add the recipient to the allowed list to send while in development." }
                      error_subcode: 2655007
                      fbtrace_id: Bx2Yz9Qw8Rt7Uv
                    InfiQ_traceid: "8796a5b4c3d2e1f00f1e2d3c4b5a6978293a4b5c6d7e8f90a1b2c3d4e5f607180123456789abcdeffedcba987654321099aabbccddeeff001122334455667788"
                reengagement_24h:
                  summary: "131047 — 24-hour window closed (use a template)"
                  value:
                    error:
                      message: "(#131047) Message failed to send because more than 24 hours have passed since the customer last replied."
                      type: OAuthException
                      code: 131047
                      error_data: { messaging_product: whatsapp, details: "Outside the 24h service window — send an approved template instead of free-form." }
                      error_subcode: 2018278
                      fbtrace_id: Cq3Wm1Zx4Nb6Ty
                    InfiQ_traceid: "293a4b5c6d7e8f90a1b2c3d4e5f607188796a5b4c3d2e1f00f1e2d3c4b5a697899aabbccddeeff00112233445566778812345678abcdef90fedcba9876543210"
                template_param_mismatch:
                  summary: "132000 — template param count mismatch"
                  value:
                    error:
                      message: "(#132000) Number of parameters does not match the expected number of params"
                      type: OAuthException
                      code: 132000
                      error_data: { messaging_product: whatsapp, details: "Template 'order_confirmation' expects 2 body params; 1 provided." }
                      error_subcode: 2494073
                      fbtrace_id: Dp4Vn2Ax5Mc7Uz
                    InfiQ_traceid: "fedcba98765432100123456789abcdef99aabbccddeeff00112233445566778893a4b5c6d7e8f90a1b2c3d4e5f607188796a5b4c3d2e1f00f1e2d3c4b5a697829"
                template_not_found:
                  summary: "132001 — template does not exist / not approved"
                  value:
                    error:
                      message: "(#132001) Template name does not exist in the translation"
                      type: OAuthException
                      code: 132001
                      error_data: { messaging_product: whatsapp, details: "No APPROVED template 'order_confirmation' in language 'en'." }
                      error_subcode: 2494072
                      fbtrace_id: Eq5Wo3By6Nd8Va
                    InfiQ_traceid: "0123456789abcdeffedcba9876543210112233445566778899aabbccddeeff00a1b2c3d4e5f60718293a4b5c6d7e8f908796a5b4c3d2e1f00f1e2d3c4b5a6978"
                undeliverable:
                  summary: "131026 — message undeliverable"
                  value:
                    error:
                      message: "(#131026) Message undeliverable"
                      type: OAuthException
                      code: 131026
                      error_data: { messaging_product: whatsapp, details: "Recipient may not have WhatsApp, or the number is unreachable." }
                      error_subcode: 2593006
                      fbtrace_id: Fr6Xp4Cz7Oe9Wb
                    InfiQ_traceid: "99aabbccddeeff00112233445566778801234567abcdef89fedcba9876543210293a4b5c6d7e8f90a1b2c3d4e5f607188796a5b4c3d2e1f00f1e2d3c4b5a6978"
                media_download_error:
                  summary: "131052 — media download error (bad link)"
                  value:
                    error:
                      message: "(#131052) Media download error"
                      type: OAuthException
                      code: 131052
                      error_data: { messaging_product: whatsapp, details: "Could not fetch the media at the provided link (must be public HTTPS)." }
                      error_subcode: 2655009
                      fbtrace_id: Gs7Yq5Da8Pf0Xc
                    InfiQ_traceid: "8796a5b4c3d2e1f00f1e2d3c4b5a6978a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00fedcba98765432100123456789abcdef"
        "401":
          description: Missing/invalid InfiQ API key, or the underlying Meta token expired.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaErrorResponse" }
              examples:
                infiq_key_invalid:
                  summary: InfiQ API key missing/invalid
                  value:
                    error:
                      message: "Invalid API key. Check the X-INFIQ-API-KEY header."
                      type: authentication_error
                      code: 0
                    InfiQ_traceid: "112233445566778899aabbccddeeff00a1b2c3d4e5f60718293a4b5c6d7e8f900f1e2d3c4b5a69788796a5b4c3d2e1f00123456789abcdeffedcba9876543210"
                meta_token_expired:
                  summary: "190 — Meta access token expired"
                  value:
                    error:
                      message: "Error validating access token: Session has expired."
                      type: OAuthException
                      code: 190
                      error_subcode: 463
                      fbtrace_id: Ht8Zr6Eb9Qg1Yd
                    InfiQ_traceid: "a1b2c3d4e5f607180123456789abcdef293a4b5c6d7e8f908796a5b4c3d2e1f00f1e2d3c4b5a6978112233445566778899aabbccddeeff00fedcba9876543210"
        "403":
          description: Blocked by policy / quality restrictions.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaErrorResponse" }
              example:
                error:
                  message: "(#368) Temporarily blocked for policies violations"
                  type: OAuthException
                  code: 368
                  error_data: { messaging_product: whatsapp, details: "Sending is temporarily restricted due to quality/policy signals." }
                  error_subcode: 1015
                  fbtrace_id: Iu9As7Fc0Rh2Ze
                InfiQ_traceid: "0f1e2d3c4b5a6978293a4b5c6d7e8f90a1b2c3d4e5f607188796a5b4c3d2e1f0fedcba9876543210112233445566778899aabbccddeeff000123456789abcdef"
        "429":
          description: Rate limit exceeded — honor X-RateLimit-Reset before retrying.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaErrorResponse" }
              example:
                error:
                  message: "(#130429) Rate limit hit"
                  type: OAuthException
                  code: 130429
                  error_data: { messaging_product: whatsapp, details: "Too many messages sent in a short period. Slow down and retry after the reset." }
                  error_subcode: 2494055
                  fbtrace_id: Jv0Bt8Gd1Si3Af
                InfiQ_traceid: "293a4b5c6d7e8f908796a5b4c3d2e1f00f1e2d3c4b5a6978a1b2c3d4e5f6071899aabbccddeeff00112233445566778801234567fedcba89abcdef9876543210"
        "500":
          description: InfiQ or Meta had an unexpected error — safe to retry with the same Idempotency-Key.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaErrorResponse" }
              example:
                error:
                  message: "(#131000) Something went wrong"
                  type: OAuthException
                  code: 131000
                  error_data: { messaging_product: whatsapp, details: "An unexpected error occurred while processing the message." }
                  error_subcode: 0
                  fbtrace_id: Kw1Cu9He2Tj4Bg
                InfiQ_traceid: "fedcba9876543210112233445566778899aabbccddeeff000123456789abcdef8796a5b4c3d2e1f00f1e2d3c4b5a6978293a4b5c6d7e8f90a1b2c3d4e5f60718"
  /templates:
    get:
      operationId: listTemplates
      tags: [Templates]
      summary: "List templates — 🚧 coming soon"
      x-status: coming-soon
      description: |
        **🚧 Coming soon — not yet available.** The shape below is the committed
        contract; calling this today returns `501 Not Implemented`. Manage
        templates in the InfiQ dashboard in the meantime.
      parameters:
        - name: status
          in: query
          required: false
          schema: { type: string, enum: [approved, pending, rejected, paused] }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: cursor
          in: query
          required: false
          description: Opaque cursor from a previous response's `next_cursor`.
          schema: { type: string }
      responses:
        "501":
          description: Not implemented yet — coming soon.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaErrorResponse" }
              example:
                error:
                  message: "Templates API is coming soon. Manage templates in the InfiQ dashboard for now."
                  type: not_implemented
                  code: 501
                InfiQ_traceid: "0123456789abcdeffedcba987654321099aabbccddeeff0011223344556677888796a5b4c3d2e1f00f1e2d3c4b5a6978293a4b5c6d7e8f90a1b2c3d4e5f60718"
        "200":
          description: "(Future) One page of templates."
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
          content:
            application/json:
              schema:
                type: object
                required: [data, next_cursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Template" }
                  next_cursor:
                    type: [string, "null"]
                    description: Pass as `cursor` to fetch the next page; null on the last page.
  /webhooks/test:
    post:
      operationId: sendTestWebhook
      tags: [Webhooks]
      summary: Send a signed test event
      description: >
        Delivers a sample event of the requested type to your registered
        endpoint(s), signed exactly like production traffic — use it to
        verify your X-InfiQ-Signature handling end to end. Works with
        sandbox keys.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event]
              properties:
                event:
                  type: string
                  description: Any cataloged event you're subscribed to — see `WebhookEvent.type` for the full list.
                  enum: [message.sent, message.delivered, message.read, message.failed, message.received, message.blocked_opt_out, message.opt_override_used, campaign.launched, campaign.completed, campaign.failed, campaign.canceled, contact.created, contact.updated, contact.deleted, template.approved, template.paused, template.disabled, apikey.created, apikey.rotated, apikey.revoked, waba.quality_changed, waba.limit_reached, waba.verified, webhook.test, billing.threshold, security.incident, system.notice]
      responses:
        "202":
          description: Test event queued for delivery.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, examples: [evt_test_1a2b] }
                  status: { type: string, const: queued }
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
webhooks:
  # ── messages (7) ──────────────────────────────────────────────────────
  message.sent:
    post:
      tags: [Webhooks]
      summary: A message was accepted for sending (public API)
      description: >
        Fired fire-and-forget right after Meta accepts a synchronous
        `POST /messages` send. This is the only producer of `message.sent`,
        and its payload is flat — there is no `data.message` wrapper like
        the delivery-time events below.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZK1A2B3C4D5E6F7G8H9J0"
              type: message.sent
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:12:39.560Z"
              livemode: true
              correlation:
                trace_id: "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
                request_id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
                actor: { type: api_key, keyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01" }
              data:
                msgId: "550e8400-e29b-41d4-a716-446655440000"
                wamid: "wamid.HBgMOTE5MTAwMDAwMDAxFQIAERgSN0FCQ0RFRjAxMjM0NTY3ODkA"
                conversationId: "550e8400-e29b-41d4-a716-446655440111"
                type: template
                sentAt: "2026-07-30T09:12:39.512Z"
              _meta:
                deliveryId: "4d6c8a02-9e1f-4b7a-8c33-2f0a9b1c7d55"
                attempt: 1
                endpointId: "e2b7c9d1-6a34-4f52-8b90-11aa22bb33cc"
                signedAt: "2026-07-30T09:12:39.560Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  message.delivered:
    post:
      tags: [Webhooks]
      summary: Message delivered to the recipient's device
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZK3M9YQN2V4W6X8Y0Z1A2"
              type: message.delivered
              apiVersion: "2026-04"
              tenantId: "b3f1c2a4-5d6e-4f70-8a91-2c3d4e5f6071"
              createdAt: "2026-07-30T09:12:41.882Z"
              livemode: true
              correlation:
                trace_id: "8de1c0a2-1f34-4b6e-9c77-0a1b2c3d4e5f"
                request_id: "4c2b1a09-8e7d-4f6a-b5c4-d3e2f1a09b8c"
                actor: { type: system }
              data:
                message:
                  id: "5f2e9a10-7c31-4b8e-9d02-6a1b2c3d4e5f"
                  tenantId: "b3f1c2a4-5d6e-4f70-8a91-2c3d4e5f6071"
                  to: "919100000001"
                  channel: whatsapp
                  direction: outbound
                  wamid: "wamid.HBgMOTE5MTAwMDAwMDAxFQIAERgSN0FCQ0RFRjAxMjM0NTY3ODkA"
                deliveredAt: "2026-07-30T09:12:41.000Z"
              _meta:
                deliveryId: "01JQ8ZK3M9YQN2V4Wcanonical"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:12:41.884Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  message.read:
    post:
      tags: [Webhooks]
      summary: Recipient read the message
      description: >
        Same shape as `message.delivered`, keyed on `readAt` instead of
        `deliveredAt`.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZK3M9YQN2V4W6X8Y0Z1B3"
              type: message.read
              apiVersion: "2026-04"
              tenantId: "b3f1c2a4-5d6e-4f70-8a91-2c3d4e5f6071"
              createdAt: "2026-07-30T09:14:03.221Z"
              livemode: true
              correlation:
                trace_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
                request_id: "6e5d4c3b-2a1f-4e0d-9c8b-7a6f5e4d3c2b"
                actor: { type: system }
              data:
                message:
                  id: "5f2e9a10-7c31-4b8e-9d02-6a1b2c3d4e5f"
                  tenantId: "b3f1c2a4-5d6e-4f70-8a91-2c3d4e5f6071"
                  to: "919100000001"
                  channel: whatsapp
                  direction: outbound
                  wamid: "wamid.HBgMOTE5MTAwMDAwMDAxFQIAERgSN0FCQ0RFRjAxMjM0NTY3ODkA"
                readAt: "2026-07-30T09:14:03.000Z"
              _meta:
                deliveryId: "01JQ8ZK4N0ZRO3W5X7Y9Z1A2B4"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:14:03.240Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  message.failed:
    post:
      tags: [Webhooks]
      summary: A message could not be delivered
      description: >
        **Two incompatible payload shapes ship under this one event name**,
        depending on which producer fired it — a receiver needs to branch on
        whether `data.message` is present. Delivery-time failures (Meta
        rejected an already-sent message) carry `data.message` +
        `data.error.{code,message,metaErrorCode}`. Send-time failures (Meta's
        synchronous send call itself threw) carry a flat
        `data.{msgId,conversationId,type,failure_reason,meta_error,failed_at}`
        with no `message` or `error` object. See both examples below.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            examples:
              delivery_time_failure:
                summary: Delivery-time failure (Meta status webhook)
                value:
                  id: "01JQ8ZK5P1AS4X6Y8Z0A2B4C6D"
                  type: message.failed
                  apiVersion: "2026-04"
                  tenantId: "b3f1c2a4-5d6e-4f70-8a91-2c3d4e5f6071"
                  createdAt: "2026-07-30T09:15:10.004Z"
                  livemode: true
                  correlation:
                    trace_id: "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f"
                    request_id: "7f6e5d4c-3b2a-4f1e-0d9c-8b7a6f5e4d3c"
                    actor: { type: system }
                  data:
                    message:
                      id: "5f2e9a10-7c31-4b8e-9d02-6a1b2c3d4e5f"
                      tenantId: "b3f1c2a4-5d6e-4f70-8a91-2c3d4e5f6071"
                      to: "919100000001"
                      channel: whatsapp
                      direction: outbound
                      wamid: "wamid.HBgMOTE5MTAwMDAwMDAxFQIAERg"
                    error: { code: "131026", message: "Message undeliverable", metaErrorCode: 131026 }
                  _meta:
                    deliveryId: "01JQ8ZK6Q2BT5Y7Z9A1B3C5D7E"
                    attempt: 1
                    endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                    signedAt: "2026-07-30T09:15:10.030Z"
              send_time_failure:
                summary: Send-time failure (public API — Meta send threw)
                value:
                  id: "01JQ8ZK7R3CU6Z8A0B2C4D6E8F"
                  type: message.failed
                  apiVersion: "2026-04"
                  tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                  createdAt: "2026-07-30T09:12:39.900Z"
                  livemode: true
                  correlation:
                    trace_id: "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a"
                    request_id: "8a7f6e5d-4c3b-4a2f-1e0d-9c8b7a6f5e4d"
                    actor: { type: api_key, keyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01" }
                  data:
                    msgId: "550e8400-e29b-41d4-a716-446655440000"
                    conversationId: "550e8400-e29b-41d4-a716-446655440111"
                    type: template
                    failure_reason: "(#131047) Re-engagement message outside 24h window"
                    meta_error: { code: 131047, type: OAuthException, error_subcode: 2494010 }
                    failed_at: "2026-07-30T09:12:39.870Z"
                  _meta:
                    deliveryId: "01JQ8ZK8S4DV7A9B1C3D5E7F9G"
                    attempt: 1
                    endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                    signedAt: "2026-07-30T09:12:39.920Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  message.received:
    post:
      tags: [Webhooks]
      summary: An inbound WhatsApp message arrived
      description: >
        `data.message.to` is the **business** number and `.from` is the
        **customer** — the reverse of the status events above. `contentType`
        carries the raw Meta message type (`image`, `audio`, `button`,
        `reaction`, …), which is broader than the four values the published
        schema enum advertises — don't hard-fail on an unrecognized value.
        `content.type` is the normalized shape discriminator; see the two
        examples below for a text and a media message.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            examples:
              text:
                summary: Text message
                value:
                  id: "01JABCDEF0123456789ABCDEFG"
                  type: message.received
                  apiVersion: "2026-04"
                  tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                  createdAt: "2026-07-30T10:15:42.113Z"
                  livemode: true
                  correlation:
                    trace_id: "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
                    request_id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
                    actor: { type: system }
                  data:
                    message:
                      id: "b3d1c0a2-1f4e-4a77-9c2b-8e2f7a1d6b90"
                      tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                      to: "+919812345670"
                      from: "+919876543210"
                      senderName: "Aditya Rao"
                      channel: whatsapp
                      direction: inbound
                      contentType: text
                      wamid: "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgUM0E1RjYyN0RCM0ExQjJDNEQ1RTYA"
                      content:
                        type: text
                        text: "Hi, is my order shipped yet?"
                        raw: { from: "919876543210", id: "wamid.HBg…", timestamp: "1785573342", type: text }
                  _meta:
                    deliveryId: "4d6c8a02-9e1f-4b7a-8c33-2f0a9b1c7d55"
                    attempt: 1
                    endpointId: "e2b7c9d1-6a34-4f52-8b90-11aa22bb33cc"
                    signedAt: "2026-07-30T10:15:42.140Z"
              image:
                summary: Image message
                value:
                  id: "01JABCDEF0123456789ABCDFHJ"
                  type: message.received
                  apiVersion: "2026-04"
                  tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                  createdAt: "2026-07-30T10:20:11.400Z"
                  livemode: true
                  correlation:
                    trace_id: "5e6f7a8b-9c0d-4e1f-2a3b-4c5d6e7f8a9b"
                    request_id: "9b8a7f6e-5d4c-4b3a-2f1e-0d9c8b7a6f5e"
                    actor: { type: system }
                  data:
                    message:
                      id: "c4e2d1b3-2a5f-4b88-0d3c-9f3a8b2e7c01"
                      tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                      to: "+919812345670"
                      from: "+919876543210"
                      senderName: "Aditya Rao"
                      channel: whatsapp
                      direction: inbound
                      contentType: image
                      wamid: "wamid.HBgMOTE5…"
                      content:
                        type: image
                        metaMediaId: "1024567890123456"
                        mimeType: image/jpeg
                        caption: "Here's the damaged box"
                        mediaUrl: "https://cpaas-prod-media.s3.ap-south-1.amazonaws.com/7c9e…/CHAT/incoming/conv-42/1785573342000_media.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=…"
                        raw: { from: "919876543210", id: "wamid.HBg…", timestamp: "1785573342", type: image }
                  _meta:
                    deliveryId: "5e7d9b13-af20-4c8e-9d44-3f1a9c2d8e56"
                    attempt: 1
                    endpointId: "e2b7c9d1-6a34-4f52-8b90-11aa22bb33cc"
                    signedAt: "2026-07-30T10:20:11.430Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  message.blocked_opt_out:
    post:
      tags: [Webhooks]
      summary: An outbound send was blocked by opt-out/consent status
      description: >
        Fired instead of sending when the recipient has opted out; no
        message row is ever created. `contactId` here is the **internal**
        contact id — `contact.created`/`contact.updated` key their `contact.id`
        on the external `contactId` instead, so join on
        `data.contactExternalId`, not `data.contactId`.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKA9V0X2Z4B6D8F0H2J4L"
              type: message.blocked_opt_out
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:12:39.500Z"
              livemode: true
              correlation:
                trace_id: "6f7a8b9c-0d1e-4f2a-3b4c-5d6e7f8a9b0c"
                request_id: "0c9b8a7f-6e5d-4c3b-2a1f-0e9d8c7b6a5f"
                actor: { type: api_key, keyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01" }
              data:
                contactId: "7a1b2c3d-4e5f-4061-8273-8495a6b7c8d9"
                contactExternalId: "ct_01HXYZ_aman"
                to: "919100000001"
                type: template
                blockedReason: marketingOptedOut
                apiKeyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01"
                honeyTokenOverrideAttempt: false
                occurredAt: "2026-07-30T09:12:39.480Z"
              _meta:
                deliveryId: "01JQ8ZKB0W1Y3A5C7E9G1I3K5M"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:12:39.520Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  message.opt_override_used:
    post:
      tags: [Webhooks]
      summary: A send bypassed opt-out via an explicit override
      description: >
        Fired when a request set `overwrite_opt: 'Y'` and the send proceeded
        anyway — a compliance audit trail for opt-out bypasses. Same
        `contactId` caveat as `message.blocked_opt_out`.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKC1X2Z4B6D8F0H2J4L6N"
              type: message.opt_override_used
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:12:39.500Z"
              livemode: true
              correlation:
                trace_id: "7a8b9c0d-1e2f-4a3b-4c5d-6e7f8a9b0c1d"
                request_id: "1d0c9b8a-7f6e-4d5c-3b2a-1f0e9d8c7b6a"
                actor: { type: api_key, keyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01" }
              data:
                contactId: "7a1b2c3d-4e5f-4061-8273-8495a6b7c8d9"
                contactExternalId: "ct_01HXYZ_aman"
                to: "919100000001"
                type: template
                optReason: marketingOptedOut
                overrideTrackingId: "INC-4821"
                apiKeyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01"
                occurredAt: "2026-07-30T09:12:39.480Z"
              _meta:
                deliveryId: "01JQ8ZKD2Y3A5C7E9G1I3K5M7O"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:12:39.520Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  # ── campaigns (4) ─────────────────────────────────────────────────────
  campaign.launched:
    post:
      tags: [Webhooks]
      summary: A broadcast campaign started sending
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKE3Z4B6D8F0H2J4L6N8P"
              type: campaign.launched
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:14:22.130Z"
              livemode: true
              correlation:
                trace_id: "8b9c0d1e-2f3a-4b4c-5d6e-7f8a9b0c1d2e"
                request_id: "2e1d0c9b-8a7f-4e6d-5c4b-3a2f1e0d9c8b"
                actor: { type: system }
              data:
                campaignId: "3f1a7c02-9b4e-4d6a-8f21-6c0b2e5a9d10"
                tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                launchedAt: "2026-07-30T09:14:22.108Z"
                audienceSize: 4820
              _meta:
                deliveryId: "01JQ8ZKF405C7E9G1I3K5M7O9Q"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:14:22.150Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  campaign.completed:
    post:
      tags: [Webhooks]
      summary: A campaign finished (fully or partially sent)
      description: >
        `metrics.sent` is the **non-failed** total (delivered + read + sent
        + replied conflated), not messages strictly in the `sent` state, and
        `metrics.delivered` is never populated even though the schema
        advertises it — don't rely on it. Can be re-emitted more than once
        for the same campaign with mutating metrics; dedupe on
        `data.campaignId` + `id`, not on "first delivery wins".
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKG516D8F0H2J4L6N8P0R"
              type: campaign.completed
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:41:55.660Z"
              livemode: true
              correlation:
                trace_id: "9c0d1e2f-3a4b-4c5d-6e7f-8a9b0c1d2e3f"
                request_id: "3f2e1d0c-9b8a-4f7e-6d5c-4b3a2f1e0d9c"
                actor: { type: system }
              data:
                campaignId: "3f1a7c02-9b4e-4d6a-8f21-6c0b2e5a9d10"
                completedAt: "2026-07-30T09:41:55.640Z"
                metrics: { sent: 4795, failed: 25 }
              _meta:
                deliveryId: "01JQ8ZKH627E9G1I3K5M7O9Q1S"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:41:55.680Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  campaign.failed:
    post:
      tags: [Webhooks]
      summary: A campaign failed outright (100% of sends failed)
      description: >
        Only fires when every message in the campaign failed — a partial
        failure is `campaign.completed` with a non-zero `metrics.failed`
        instead. `reason` is always the same generated sentence; it does not
        carry granular failure causes.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKJ738F0H2J4L6N8P0R2T"
              type: campaign.failed
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:41:55.660Z"
              livemode: true
              correlation:
                trace_id: "0d1e2f3a-4b5c-4d6e-7f8a-9b0c1d2e3f4a"
                request_id: "4a3b2c1d-0e9f-4a8b-7c6d-5e4f3a2b1c0d"
                actor: { type: system }
              data:
                campaignId: "3f1a7c02-9b4e-4d6a-8f21-6c0b2e5a9d10"
                failedAt: "2026-07-30T09:41:55.640Z"
                reason: "All 4820 messages failed to send"
              _meta:
                deliveryId: "01JQ8ZKK849G1I3K5M7O9Q1S3U"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:41:55.680Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  campaign.canceled:
    post:
      tags: [Webhooks]
      summary: A draft or scheduled campaign was canceled
      description: >
        Only `DRAFT` or pending `SCHEDULED` campaigns can be canceled — a
        `RUNNING` campaign can never emit this event.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKM95AH2J4L6N8P0R2T4V"
              type: campaign.canceled
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:20:03.530Z"
              livemode: true
              correlation:
                trace_id: "1e2f3a4b-5c6d-4e7f-8a9b-0c1d2e3f4a5b"
                request_id: "5b4a3c2d-1e0f-4b9c-8d7e-6f5a4b3c2d1e"
                actor: { type: system }
              data:
                campaignId: "3f1a7c02-9b4e-4d6a-8f21-6c0b2e5a9d10"
                canceledAt: "2026-07-30T09:20:03.512Z"
                canceledBy: "a19c33f0-2b77-49aa-9f0e-11d5c2b7e4aa"
              _meta:
                deliveryId: "01JQ8ZKN06BI3K5M7O9Q1S3U5W"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:20:03.550Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  # ── contacts (3) ──────────────────────────────────────────────────────
  contact.created:
    post:
      tags: [Webhooks]
      summary: A contact was created (manual or auto-created from an inbound message)
      description: >
        `data.contact.id` is the **external**, tenant-facing contact id —
        not the same id space `message.blocked_opt_out`/`opt_override_used`
        use for `data.contactId` (see those events' notes).
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JZ8QK9F3M2N7P4R6T8W0Y2XB"
              type: contact.created
              apiVersion: "2026-04"
              tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
              createdAt: "2026-07-30T09:14:52.118Z"
              livemode: true
              correlation:
                trace_id: "c9a1f4e2-1b7d-4e83-9f0a-2c6b5d8e1a90"
                request_id: "e2d7b1c4-8a35-4f61-b0d9-3f7a1e5c2b48"
                actor: { type: system }
              data:
                contact:
                  id: "b2e4a1c8-9f3d-4e7a-8c21-6d5b0a9f2e17"
                  tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
                  phone: "+919876543210"
                  name: "Priya Sharma"
                  tags: []
                  createdAt: "2026-07-30T09:14:52.090Z"
                  updatedAt: "2026-07-30T09:14:52.090Z"
              _meta:
                deliveryId: "01JZ8QK9G7X4A2B6C8D0E1F3H5"
                attempt: 1
                endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                signedAt: "2026-07-30T09:14:52.140Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  contact.updated:
    post:
      tags: [Webhooks]
      summary: A contact was updated
      description: >
        Three different code paths emit this event and they disagree on
        `changes[]`: a manual `PATCH` lists `{field,to}` per changed key
        (never `from`); an inbound `STOP`/`START` opt-in toggle emits a
        single `{field:'optedIn',to}` entry (`optedIn` isn't otherwise on
        the `contact` object, so this is the only place to read it); an
        inbound contact-detail patch always sends `changes: []` even though
        fields did change. Don't rely on `changes[].from` — no producer
        populates it.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            examples:
              manual_patch:
                summary: Manual PATCH (rename + retag)
                value:
                  id: "01JZ8R2M4V7C9K1P3S5W7Y9B0D"
                  type: contact.updated
                  apiVersion: "2026-04"
                  tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
                  createdAt: "2026-07-30T10:02:11.774Z"
                  livemode: true
                  correlation:
                    trace_id: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
                    request_id: "9f8e7d6c-5b4a-4938-8271-6f5e4d3c2b1a"
                    actor: { type: system }
                  data:
                    contact:
                      id: "b2e4a1c8-9f3d-4e7a-8c21-6d5b0a9f2e17"
                      tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
                      phone: "+919876543210"
                      name: "Priya S."
                      tags: [vip, mumbai]
                      createdAt: "2026-07-30T09:14:52.090Z"
                      updatedAt: "2026-07-30T10:02:11.702Z"
                    changes:
                      - { field: name, to: "Priya S." }
                      - { field: tags, to: [vip, mumbai] }
                  _meta:
                    deliveryId: "01JZ8R2M5H3D8F0J2L4N6Q8S0U"
                    attempt: 1
                    endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                    signedAt: "2026-07-30T10:02:11.790Z"
              opt_out_toggle:
                summary: Inbound STOP (opt-out)
                value:
                  id: "01JZ8R3N5W8D0L2Q4S6U8W0Z2F"
                  type: contact.updated
                  apiVersion: "2026-04"
                  tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
                  createdAt: "2026-07-30T11:20:03.470Z"
                  livemode: true
                  correlation:
                    trace_id: "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
                    request_id: "0e9d8c7b-6a5f-4e4d-3c2b-1a0f9e8d7c6b"
                    actor: { type: system }
                  data:
                    contact:
                      id: "b2e4a1c8-9f3d-4e7a-8c21-6d5b0a9f2e17"
                      tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
                      phone: "+919876543210"
                      name: "Priya Sharma"
                      tags: []
                      createdAt: "2026-07-30T09:14:52.090Z"
                      updatedAt: "2026-07-30T11:20:03.451Z"
                    changes:
                      - { field: optedIn, to: false }
                  _meta:
                    deliveryId: "01JZ8R4P6X9E1M3R5T7V9X1A3G"
                    attempt: 1
                    endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                    signedAt: "2026-07-30T11:20:03.490Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  contact.deleted:
    post:
      tags: [Webhooks]
      summary: A contact was deleted
      description: >
        **Not currently emitted.** This event is cataloged and its schema is
        published (so it validates on subscribe and appears in the schema
        endpoint), but the only delete code path is commented out in the
        backend — no producer dispatches it today. The shape below is
        derived from the published schema, not from an observed delivery.
        Subscribing to it is safe but will not deliver anything until the
        backend wires up a producer.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JZ8R5Q7Y0F2N4S6U8W0Y2B4H"
              type: contact.deleted
              apiVersion: "2026-04"
              tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
              createdAt: "2026-07-30T12:00:00.020Z"
              livemode: true
              correlation:
                trace_id: "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
                request_id: "1f0e9d8c-7b6a-4f5e-4d3c-2b1a0f9e8d7c"
                actor: { type: system }
              data:
                contactId: "b2e4a1c8-9f3d-4e7a-8c21-6d5b0a9f2e17"
                tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
                deletedAt: "2026-07-30T12:00:00.000Z"
              _meta:
                deliveryId: "01JZ8R6R8Z1G3O5T7V9X1A3C5I"
                attempt: 1
                endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                signedAt: "2026-07-30T12:00:00.040Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  # ── templates (3) ─────────────────────────────────────────────────────
  template.approved:
    post:
      tags: [Webhooks]
      summary: A template was approved by Meta
      description: >
        `templateId` is InfiQ's internal template UUID, not Meta's
        `message_template_id` — correlate with Meta by `name` + `language`
        instead.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JZ8SB1N4Q6T8W0Y2A4C6E8G0"
              type: template.approved
              apiVersion: "2026-04"
              tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
              createdAt: "2026-07-30T13:40:07.512Z"
              livemode: true
              correlation:
                trace_id: "11112222-3333-4444-5555-666677778888"
                request_id: "88887777-6666-5555-4444-333322221111"
                actor: { type: system }
              data:
                templateId: "3a9c7f21-8b4e-4c1d-9a6f-2e5b8d0c1a34"
                name: order_confirmation
                language: en_US
                approvedAt: "2026-07-30T13:40:07.512Z"
              _meta:
                deliveryId: "01JZ8SB1P2R5T7V9X1Z3B5D7F9"
                attempt: 1
                endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                signedAt: "2026-07-30T13:40:07.540Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  template.paused:
    post:
      tags: [Webhooks]
      summary: A template was paused by Meta
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JZ8SC2P5R7U9W1Y3B5D7F9H1"
              type: template.paused
              apiVersion: "2026-04"
              tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
              createdAt: "2026-07-30T14:05:22.340Z"
              livemode: true
              correlation:
                trace_id: "22223333-4444-5555-6666-777788889999"
                request_id: "99998888-7777-6666-5555-444433332222"
                actor: { type: system }
              data:
                templateId: "3a9c7f21-8b4e-4c1d-9a6f-2e5b8d0c1a34"
                name: order_confirmation
                pausedAt: "2026-07-30T14:05:22.318Z"
                reason: "This template has been paused due to low quality."
              _meta:
                deliveryId: "01JZ8SC2Q6S8V0X2Z4C6E8G0J2"
                attempt: 1
                endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                signedAt: "2026-07-30T14:05:22.360Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  template.disabled:
    post:
      tags: [Webhooks]
      summary: A template was disabled by Meta
      description: >
        `reason` is present when Meta supplied one; when Meta sends none the
        key is **omitted entirely** (not `null`) — unlike `template.paused`,
        which always defaults to a placeholder reason.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JZ8SD3Q7T9V1X3Z5D7F9H1K3"
              type: template.disabled
              apiVersion: "2026-04"
              tenantId: "7f3c1e90-2a4b-4d6e-9c81-0b5a2f8e1d34"
              createdAt: "2026-07-30T14:31:09.030Z"
              livemode: true
              correlation:
                trace_id: "33334444-5555-6666-7777-888899990000"
                request_id: "00009999-8888-7777-6666-555544443333"
                actor: { type: system }
              data:
                templateId: "3a9c7f21-8b4e-4c1d-9a6f-2e5b8d0c1a34"
                name: order_confirmation
                disabledAt: "2026-07-30T14:31:09.004Z"
                reason: "Repeated policy violations"
              _meta:
                deliveryId: "01JZ8SD3R8U0W2Y4A6E8G0J2L4"
                attempt: 1
                endpointId: "d5f80c3a-6e21-4b9d-8a17-0c4e2f6b91d3"
                signedAt: "2026-07-30T14:31:09.050Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  # ── apiKeys (3) ───────────────────────────────────────────────────────
  apikey.created:
    post:
      tags: [Webhooks]
      summary: A new API key was created
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKP1A2C4E6G8I0K2M4O6Q"
              type: apikey.created
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T08:59:00.020Z"
              livemode: true
              correlation:
                trace_id: "44445555-6666-7777-8888-999900001111"
                request_id: "11110000-9999-8888-7777-666655554444"
                actor: { type: user, userId: "a19c33f0-2b77-49aa-9f0e-11d5c2b7e4aa" }
              data:
                keyId: "c0ffee11-2233-4455-6677-8899aabbccdd"
                prefix: "ik_live_a1b2c3d4"
                environment: live
                scopes: ["messages:send", "templates:read", "contacts:write"]
                createdAt: "2026-07-30T08:59:00.000Z"
              _meta:
                deliveryId: "01JQ8ZKQ2B3D5F7H9J1L3N5P7R"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T08:59:00.040Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  apikey.rotated:
    post:
      tags: [Webhooks]
      summary: An API key was rotated
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKR3C4E6G8J0L2N4P6R8T"
              type: apikey.rotated
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T10:05:12.320Z"
              livemode: true
              correlation:
                trace_id: "55556666-7777-8888-9999-000011112222"
                request_id: "22221111-0000-9999-8888-777766665555"
                actor: { type: user, userId: "a19c33f0-2b77-49aa-9f0e-11d5c2b7e4aa" }
              data:
                oldKeyId: "c0ffee11-2233-4455-6677-8899aabbccdd"
                newKeyId: "d1a5f00d-9911-4a2b-8c3d-77e6b5a4c3b2"
                rotatedAt: "2026-07-30T10:05:12.300Z"
                gracePeriodUntil: "2026-07-30T10:35:12.300Z"
              _meta:
                deliveryId: "01JQ8ZKS4D5F7H9K1M3O5Q7S9U"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T10:05:12.340Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  apikey.revoked:
    post:
      tags: [Webhooks]
      summary: An API key was revoked
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKT5E6G8J0L2N4Q6S8U0W"
              type: apikey.revoked
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T10:12:40.200Z"
              livemode: true
              correlation:
                trace_id: "66667777-8888-9999-0000-111122223333"
                request_id: "33332222-1111-0000-9999-888877776666"
                actor: { type: user, userId: "a19c33f0-2b77-49aa-9f0e-11d5c2b7e4aa" }
              data:
                keyId: "c0ffee11-2233-4455-6677-8899aabbccdd"
                reason: compromised_key_rotation
                revokedAt: "2026-07-30T10:12:40.180Z"
                revokedBy: "a19c33f0-2b77-49aa-9f0e-11d5c2b7e4aa"
              _meta:
                deliveryId: "01JQ8ZKU6F7H9K1N3P5R7T9V1X"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T10:12:40.220Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  # ── waba (3) ──────────────────────────────────────────────────────────
  waba.quality_changed:
    post:
      tags: [Webhooks]
      summary: A phone number's quality rating changed
      description: >
        Meta's raw `GREEN/YELLOW/RED/RED_FLAGGED` rating is mapped to
        `HIGH/MEDIUM/LOW/FLAGGED` before dispatch; only fires when the
        mapped rating actually changed.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKV7G8J0L2N4Q6T8V0X2Z"
              type: waba.quality_changed
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:30:10.030Z"
              livemode: true
              correlation:
                trace_id: "77778888-9999-0000-1111-222233334444"
                request_id: "44443333-2222-1111-0000-999988887777"
                actor: { type: system }
              data:
                wabaId: "109876543210987"
                phoneNumberId: "123456789012345"
                qualityFrom: HIGH
                qualityTo: MEDIUM
                changedAt: "2026-07-30T09:30:10.005Z"
              _meta:
                deliveryId: "01JQ8ZKW8H9K1N3Q5S7U9W1Y3A"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:30:10.050Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  waba.limit_reached:
    post:
      tags: [Webhooks]
      summary: A messaging limit blocked a campaign launch
      description: >
        Fired immediately before the launch request is rejected with a 400.
        `limitType` is currently always `daily_conversation` in practice,
        even though the schema enum also allows `rate_limit`/`tier_cap`.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKX9J0L2N4Q6T8V0X2Z4B"
              type: waba.limit_reached
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:14:21.920Z"
              livemode: true
              correlation:
                trace_id: "88889999-0000-1111-2222-333344445555"
                request_id: "55554444-3333-2222-1111-000099998888"
                actor: { type: system }
              data:
                wabaId: "109876543210987"
                limitType: daily_conversation
                reachedAt: "2026-07-30T09:14:21.900Z"
              _meta:
                deliveryId: "01JQ8ZKY0K1N3Q5S7V9X1Z3B5D"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:14:21.940Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  waba.verified:
    post:
      tags: [Webhooks]
      summary: A WhatsApp Business Account passed Meta's review
      description: Fired only when Meta's account-review decision is `APPROVED`.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZKZ1L2P4R6T8V0X2Z4B6D"
              type: waba.verified
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:35:44.240Z"
              livemode: true
              correlation:
                trace_id: "99990000-1111-2222-3333-444455556666"
                request_id: "66665555-4444-3333-2222-111100009999"
                actor: { type: system }
              data:
                wabaId: "109876543210987"
                verifiedAt: "2026-07-30T09:35:44.220Z"
              _meta:
                deliveryId: "01JQ8ZM02M3Q5S7V9X1Z3B5D7F"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:35:44.260Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  # ── system (4) ────────────────────────────────────────────────────────
  webhook.test:
    post:
      tags: [Webhooks]
      summary: A signed test ping, triggered from the dashboard or `POST /webhooks/test`
      description: >
        The only "meta" event that is a real, signed HTTP delivery (the
        others — circuit-breaker/DLQ/replay-progress notifications — are
        dashboard-only and never reach your endpoint). `id` is a UUID here,
        not a ULID like the catalog events above.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
              type: webhook.test
              apiVersion: "2026-04"
              tenantId: "7b3f1c9a-2d5e-4a10-9f8b-1c2d3e4f5a6b"
              createdAt: "2026-07-30T11:42:07.318Z"
              livemode: false
              correlation:
                trace_id: "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
                request_id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
                actor: { type: user, userId: "u_op_017" }
              data:
                message: ping
                endpointId: "e2b7c9d1-6a34-4f52-8b90-11aa22bb33cc"
                sentAt: "2026-07-30T11:42:07.318Z"
              _meta:
                deliveryId: "4d6c8a02-9e1f-4b7a-8c33-2f0a9b1c7d55"
                attempt: 1
                endpointId: "e2b7c9d1-6a34-4f52-8b90-11aa22bb33cc"
                signedAt: "2026-07-30T11:42:07.319Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  billing.threshold:
    post:
      tags: [Webhooks]
      summary: Monthly spend crossed a billing threshold
      description: >
        **Not currently emitted.** Cataloged and schema-published, but no
        code path dispatches it — the shape below is derived from the
        published schema, not from an observed delivery. It's the only
        money-scaled event in the catalog; until a producer exists,
        subscribing to it will never deliver anything.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZM13N4S6U8W0Y2A4C6E8G"
              type: billing.threshold
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T12:00:00.020Z"
              livemode: true
              correlation:
                trace_id: "00001111-2222-3333-4444-555566667777"
                request_id: "77776666-5555-4444-3333-222211110000"
                actor: { type: system }
              data:
                threshold: 80pct
                currentSpendInr: 40000
                monthlyCapInr: 50000
                reachedAt: "2026-07-30T12:00:00.000Z"
              _meta:
                deliveryId: "01JQ8ZM24O5V7X9Z1B3D5F7H9J"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T12:00:00.040Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  security.incident:
    post:
      tags: [Webhooks]
      summary: A security-relevant event was detected on your tenant
      description: >
        Only 2 of the 5 internal producers of this event actually reach
        outbound webhooks (the other 3 — honey-token trips, audit-chain
        tampering, DPDP breach reports — are dashboard-only WebSocket
        notifications with incompatible field shapes). If you rely on this
        webhook for security monitoring, know that it will not cover those
        three cases today.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZM35P6W8Y0Z2C4E6G8J0L"
              type: security.incident
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T09:50:00.020Z"
              livemode: true
              correlation:
                trace_id: "11112222-3333-4444-5555-666677778888"
                request_id: "88887777-6666-5555-4444-333322221111"
                actor: { type: system }
              data:
                kind: brute_force_lockout
                severity: high
                detectedAt: "2026-07-30T09:50:00.000Z"
                details:
                  resourceId: "c0ffee11-2233-4455-6677-8899aabbccdd"
                  resourceType: apikey
                  title: "API key locked after 10 failed auth attempts"
              _meta:
                deliveryId: "01JQ8ZM46Q7X9Z1B3D5F7H9K1N"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T09:50:00.040Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
  system.notice:
    post:
      tags: [Webhooks]
      summary: A general platform notice
      description: >
        The payload actually shipped by the one producer of this event
        (public template submission) does **not** match its own published
        schema — the schema requires `notice`/`level`/`noticedAt`, none of
        which are ever sent; the real fields are shown below. Treat the
        schema-advertised shape as aspirational, not current behavior.
      parameters:
        - { $ref: "#/components/parameters/InfiQSignature" }
        - { $ref: "#/components/parameters/InfiQEventId" }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              id: "01JQ8ZM57R8Y0Z2C4E6G8J0L2N"
              type: system.notice
              apiVersion: "2026-04"
              tenantId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              createdAt: "2026-07-30T10:10:00.020Z"
              livemode: true
              correlation:
                trace_id: "22223333-4444-5555-6666-777788889999"
                request_id: "99998888-7777-6666-5555-444433332222"
                actor: { type: api_key, keyId: "a4b5c6d7-e8f9-4012-8345-6789abcdef01" }
              data:
                code: TEMPLATE_SUBMITTED
                template_id: "tmpl_7a1b2c3d4e5f"
                meta_template_id: "1234567890123456"
                name: order_confirmation_v3
                language: en_US
                category: UTILITY
                submitted_at: "2026-07-30T10:10:00.000Z"
              _meta:
                deliveryId: "01JQ8ZM68S9Z1B3D5F7H9K1P3R"
                attempt: 1
                endpointId: "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9"
                signedAt: "2026-07-30T10:10:00.040Z"
      responses:
        "200":
          { description: "Return any 2xx quickly; retries continue for 24h otherwise." }
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-INFIQ-API-KEY
      description: >
        Scoped, rotatable API key from Settings → Developers. Live keys
        `isk_live_…`, sandbox keys `isk_test_…`. Examples in this spec never
        contain real keys.
  parameters:
    InfiQSignature:
      name: X-InfiQ-Signature
      in: header
      required: true
      description: >
        `t=<unix_seconds>,v1=<hex hmac>` — see the Webhooks tag description
        for the full verification recipe and the rest of the header set
        (`X-InfiQ-Timestamp`, `X-InfiQ-Tenant`, `X-InfiQ-Live`, …).
      schema: { type: string, examples: ["t=1785540127,v1=5f8c…"] }
    InfiQEventId:
      name: X-InfiQ-Event-Id
      in: header
      required: true
      description: >
        The event id (ULID), stable across retries — use this to dedupe, not
        `X-InfiQ-Event` (which despite its name carries the same id, not the
        event type).
      schema: { type: string, examples: ["01JQ8ZK3M9YQN2V4W6X8Y0Z1A2"] }
  headers:
    X-RateLimit-Limit:
      description: Requests allowed in the current window for this key.
      schema: { type: integer }
    X-RateLimit-Remaining:
      description: Requests remaining in the current window.
      schema: { type: integer }
    X-RateLimit-Reset:
      description: Unix timestamp (seconds) when the window resets.
      schema: { type: integer }
  schemas:
    InfiQTraceId:
      type: string
      description: >
        128-character lowercase hex (64 bytes from a CSPRNG) minted by InfiQ on
        EVERY response — success and error alike. Quote it to support to
        correlate a call with InfiQ's logs. It is InfiQ's, not Meta's
        (Meta's own correlation id is `error.fbtrace_id`).
      pattern: "^[0-9a-f]{128}$"
      examples:
        - "a1b2c3d4e5f60718293a4b5c6d7e8f900f1e2d3c4b5a69788796a5b4c3d2e1f0112233445566778899aabbccddeeff000123456789abcdeffedcba9876543210"
    SendMessageRequest:
      type: object
      description: >
        The exact Meta WhatsApp Cloud API `/messages` body (passthrough). Set
        `type` and the matching object (text/template/image/video/audio/
        document/sticker/location/contacts/interactive/reaction). For read
        receipts and typing indicators use `status: read` + `message_id`
        instead of `type`. See the examples dropdown for every shape.
      required: [messaging_product]
      additionalProperties: true
      properties:
        messaging_product: { type: string, const: whatsapp }
        recipient_type: { type: string, enum: [individual], default: individual }
        to:
          type: string
          description: Recipient MSISDN — country code + number (e.g. 919820000001). E.164 with a leading + is also accepted.
          examples: ["919820000001"]
        type:
          type: string
          enum: [text, template, image, video, audio, document, sticker, location, contacts, interactive, reaction]
          description: Message type. Omit when sending a `status` receipt.
        context:
          type: object
          description: Quote/reply to an inbound message.
          properties:
            message_id: { type: string }
        status:
          type: string
          enum: [read]
          description: Set to `read` (with `message_id`) to send a read receipt / typing indicator instead of a message.
        message_id: { type: string, description: Inbound message id — for receipts and reactions. }
        biz_opaque_callback_data:
          type: string
          description: Opaque tracking string echoed back on status webhooks.
        text: { type: object, additionalProperties: true }
        template: { type: object, additionalProperties: true }
        image: { type: object, additionalProperties: true }
        video: { type: object, additionalProperties: true }
        audio: { type: object, additionalProperties: true }
        document: { type: object, additionalProperties: true }
        sticker: { type: object, additionalProperties: true }
        location: { type: object, additionalProperties: true }
        contacts: { type: array, items: { type: object, additionalProperties: true } }
        interactive: { type: object, additionalProperties: true }
        reaction: { type: object, additionalProperties: true }
        typing_indicator: { type: object, additionalProperties: true }
    SendMessageResponse:
      type: object
      description: Meta's `/messages` response, verbatim, plus `InfiQ_traceid`.
      required: [InfiQ_traceid]
      properties:
        messaging_product: { type: string, const: whatsapp }
        contacts:
          type: array
          items:
            type: object
            properties:
              input: { type: string, description: The recipient you sent to. }
              wa_id: { type: string, description: Meta's canonical WhatsApp id for that recipient. }
        messages:
          type: array
          items:
            type: object
            properties:
              id: { type: string, description: "Meta message id (wamid.…) — echoed on delivery webhooks." }
              message_status:
                type: string
                description: Meta's initial acceptance status.
                enum: [accepted, held_for_quality_assessment]
        success:
          type: boolean
          description: Returned instead of contacts/messages for `status` (read receipt / typing) requests.
        InfiQ_traceid: { $ref: "#/components/schemas/InfiQTraceId" }
    MetaErrorResponse:
      type: object
      description: Meta's error object, verbatim, plus `InfiQ_traceid`.
      required: [error, InfiQ_traceid]
      properties:
        error:
          type: object
          required: [message]
          properties:
            message: { type: string, description: "Human-readable Meta error, usually prefixed with (#code)." }
            type: { type: string, examples: [OAuthException] }
            code: { type: integer, description: "Meta error code (e.g. 131030, 132000, 190)." }
            error_subcode: { type: integer }
            error_data:
              type: object
              properties:
                messaging_product: { type: string }
                details: { type: string, description: The most actionable field — read this first. }
            fbtrace_id: { type: string, description: Meta's own trace id (distinct from InfiQ_traceid). }
        InfiQ_traceid: { $ref: "#/components/schemas/InfiQTraceId" }
    Template:
      type: object
      required: [name, category, language, status]
      properties:
        name: { type: string, examples: [order_shipped] }
        category:
          type: string
          enum: [marketing, utility, authentication]
          description: Meta template category (drives per-message pricing).
        language: { type: string, examples: [en] }
        status: { type: string, enum: [approved, pending, rejected, paused] }
    WebhookEvent:
      type: object
      description: >
        The envelope wrapping every webhook delivery — identical shape for
        all 27 cataloged events; only `type` and `data` vary. `data` is
        event-specific; see each event under the Webhooks tag for its exact
        shape and caveats.
      required: [id, type, apiVersion, tenantId, createdAt, livemode, correlation, data, _meta]
      properties:
        id:
          type: string
          description: >
            Stable id for this logical event — a ULID for catalog events, a
            UUID for `webhook.test`. Retries redeliver the same `id`; dedupe
            on it (it's also echoed as `X-InfiQ-Event-Id` and
            `Idempotency-Key`), not on `_meta.deliveryId`, which changes per
            delivery attempt.
          examples: ["01JR8Z9M4KP7QW6VN3B0YX2HTE"]
        type:
          type: string
          description: Which of the 27 cataloged events this is.
          enum:
            [
              message.sent,
              message.delivered,
              message.read,
              message.failed,
              message.received,
              message.blocked_opt_out,
              message.opt_override_used,
              campaign.launched,
              campaign.completed,
              campaign.failed,
              campaign.canceled,
              contact.created,
              contact.updated,
              contact.deleted,
              template.approved,
              template.paused,
              template.disabled,
              apikey.created,
              apikey.rotated,
              apikey.revoked,
              waba.quality_changed,
              waba.limit_reached,
              waba.verified,
              webhook.test,
              billing.threshold,
              security.incident,
              system.notice,
            ]
        apiVersion:
          type: string
          description: Envelope/contract version this delivery was built against.
          examples: ["2026-04"]
        tenantId: { type: string, format: uuid, description: Your tenant id. }
        createdAt:
          type: string
          format: date-time
          description: >
            Envelope-build time, not necessarily the moment the underlying
            event happened — producers rarely set this explicitly, so it's
            usually the dispatch timestamp.
        livemode:
          type: boolean
          description: True when this endpoint is in live mode, false for a sandbox/test endpoint.
        correlation:
          type: object
          required: [trace_id, request_id, actor]
          properties:
            trace_id:
              type: string
              format: uuid
              description: Also carried in the `traceparent` header when present. Random per delivery when the producer didn't set one — most don't.
            request_id: { type: string, format: uuid }
            actor:
              type: object
              required: [type]
              description: Who/what caused the event. Defaults to `system` whenever the producer omits it — including some user-initiated actions (e.g. `campaign.canceled`).
              properties:
                type: { type: string, enum: [system, user, api_key] }
                userId: { type: string, description: Present when `type` is `user`. }
                keyId: { type: string, description: Present when `type` is `api_key`. }
        data:
          type: object
          description: >
            Event-specific payload — see each event below for its exact
            fields. Not schema-validated before dispatch, so producer bugs
            (missing fields, wrong casing) ship as-is; see individual event
            descriptions for known discrepancies.
          additionalProperties: true
        previous:
          type: object
          description: Prior-state snapshot for change-data-capture events. Omitted (not null) when absent — currently no producer sets it.
          additionalProperties: true
        _meta:
          type: object
          required: [deliveryId, attempt, endpointId, signedAt]
          properties:
            deliveryId: { type: string, description: This delivery attempt's id. }
            attempt:
              type: integer
              minimum: 1
              description: >
                Hard-coded to `1` on every delivery, including retries — it
                is not a reliable retry counter. The true attempt number is
                only in the `X-InfiQ-Attempt` header.
            endpointId: { type: string, format: uuid, description: The endpoint this copy was sent to. }
            signedAt: { type: string, format: date-time, description: "Envelope-build sign time — distinct from the HMAC's own `t=` timestamp on retries." }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, message]
          properties:
            type:
              type: string
              enum: [authentication_error, validation_error, rate_limit_error, api_error]
            message: { type: string }
            errors:
              type: array
              description: Field-level details on validation_error.
              items:
                type: object
                properties:
                  field: { type: string }
                  message: { type: string }
