{
  "openapi": "3.1.0",
  "info": {
    "title": "Genuka WA API",
    "version": "1.1.7",
    "summary": "REST API for the official WhatsApp Business Platform (Meta Cloud API): notifications, one-time codes, campaigns and signed webhooks on your own WhatsApp Business number.",
    "description": "Genuka WA is a REST API for the official WhatsApp Business Platform (Meta's Cloud API). Genuka is a Meta Tech Provider: you connect your own WhatsApp Business number through Meta's Embedded Signup, then send notifications, one-time codes and campaigns over HTTP and receive replies and delivery statuses on signed webhooks, without applying to Meta as a provider yourself. Meta's message rates are billed by Meta with no markup from Genuka; Genuka charges a subscription per WhatsApp number.\n\n## Authentication\n\nEvery request carries a server-side API key as a bearer token:\n\n```\nAuthorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nGenerate keys in the dashboard under **API keys**; a key is shown only once. A key is either partner-wide (every client and number on the account) or scoped to one client. A client-scoped key only resolves its own client: lists return its rows only, an id belonging to another client answers `404`, and the billing endpoints answer `403 partner_key_required`. A key can send from every number it covers: call the API from your backend only, never from a browser or a mobile app.\n\n## Conventions\n\n- Base URL `https://wa.genuka.com`, every endpoint under `/api/v1`. Call it over https and without a trailing slash: `http://` answers `301` and a trailing slash `308`, and some HTTP clients drop the `Authorization` header when they follow a redirect.\n- JSON in and out. List endpoints return `{ \"data\": [...] }`.\n- Failures return `{ \"error\": \"<code>\", \"message\": \"...\" }` with an HTTP status. Branch on `error`, which is stable; `message` is prose. When Meta refused the call, `meta` carries Meta's error class, code and `traceId`.\n- Every response from the API carries an `x-request-id` header; quote it when asking for support.\n- Send a descriptive `User-Agent`. The CDN in front of the API answers `403` with the plain-text body `error code: 1010` to a few bot signatures, most often Python's default `urllib` one.\n- A `200` on `POST /api/v1/messages` means Meta accepted the message. Delivery, read and failure statuses, and customer replies, arrive on your webhooks: register an endpoint with `POST /api/v1/webhooks`.\n\n## Learn more\n\n- Documentation: https://wa.genuka.com/docs (English: https://wa.genuka.com/en/docs)\n- Quickstart: https://wa.genuka.com/en/docs/quickstart\n- For AI agents: https://wa.genuka.com/llms.txt, and the MCP server `@genuka/whatsapp-mcp` (https://wa.genuka.com/en/docs/guides/ai-agents)\n- TypeScript SDK `@genuka/whatsapp`: https://wa.genuka.com/sdk/messages",
    "contact": {
      "name": "Genuka WA",
      "url": "https://wa.genuka.com"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "externalDocs": {
    "description": "Genuka WA documentation: quickstart, guides, webhooks and error reference.",
    "url": "https://wa.genuka.com/en/docs/api"
  },
  "servers": [
    {
      "url": "https://wa.genuka.com",
      "description": "Production. Every endpoint lives under /api/v1."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Messages",
      "description": "Send WhatsApp messages from a connected number. An approved template (notification, one-time code, re-engagement) can be sent at any time; text, media, location, contacts, reactions and interactive messages are delivered only inside the 24-hour customer service window opened by the customer's last message. A `200` means Meta accepted the message: delivery and read statuses, and customer replies, arrive on your webhooks.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/messages"
      }
    },
    {
      "name": "Templates",
      "description": "Create, edit, delete and sync message templates on the client's WhatsApp Business Account, browse Meta's library of pre-approved templates, and read per-template analytics. Every template is reviewed by Meta. AUTHENTICATION templates (one-time codes) require the client's business to have passed Meta business verification (or another Meta scaling path); MARKETING and UTILITY templates do not.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Media",
      "description": "Store a file once and send it many times by `assetId`, or get the `header_handle` that creating a template with a media header requires. Files are deleted 30 days after upload, and storage counts against the plan.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Campaigns",
      "description": "The bulk-send primitive: a campaign is one template plus a recipient list, each recipient with its own variables, launched with one call and tracked per recipient. Genuka WA has no campaign console; campaigns are driven entirely through these endpoints.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/guides/campaigns-api"
      }
    },
    {
      "name": "Flows",
      "description": "Create, update, preview and publish WhatsApp Flows (multi-screen forms that open inside the chat). A published Flow is sent with `POST /api/v1/messages` through `raw`.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Webhooks",
      "description": "Manage the HTTPS endpoints that receive events: inbound customer messages, delivery statuses, template reviews, account and number changes. Every delivery is signed with HMAC-SHA256 in `X-Genuka-Signature`, retried after 1 min, 5 min, 30 min, 2 h and 6 h when your endpoint does not answer 2xx, and can be replayed. The payload itself is described under the `event` webhook.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/webhooks"
      }
    },
    {
      "name": "Connections",
      "description": "The WhatsApp connections the key can reach: one WhatsApp Business Account plus one phone number each. A connection's `id` is the `connectionId` that sending and every number-level endpoint expect. Numbers are connected by the business itself through Meta's Embedded Signup (a `/connect/<slug>` link), not through this API; a number already used in the WhatsApp Business app can be kept (coexistence).",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/onboarding"
      }
    },
    {
      "name": "Numbers",
      "description": "Health of the connected numbers: Meta quality rating, messaging limit tier, throughput, and whether the number is shared with the WhatsApp Business app (coexistence).",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Companies",
      "description": "The clients (businesses) whose numbers are connected to the account. A partner-wide key sees every client; a client-scoped key sees only its own.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Business profile",
      "description": "The WhatsApp Business profile customers see when they tap the business name (about line, address, description, email, websites, category), read and written live on Meta.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Conversation settings",
      "description": "A number's conversational automation: the welcome-message trigger, ice breakers and slash commands.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Blocked users",
      "description": "Block and unblock WhatsApp users on a number, with an itemised outcome per number.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "QR codes",
      "description": "Permanent `wa.me` short links and QR codes that open a chat with the number and a prefilled message.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Analytics",
      "description": "Meta's messaging, conversation and pricing analytics for the WhatsApp Business Account behind a number, plus Genuka's per-day archive for data older than Meta's 365-day window.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/api"
      }
    },
    {
      "name": "Billing",
      "description": "The Genuka subscription: plan, limits, usage and invoices. Partner-wide keys only. Genuka charges a subscription per WhatsApp number; Meta bills its message rates directly to the client's WhatsApp Business Account, with no markup from Genuka, so Meta charges never appear here.",
      "externalDocs": {
        "url": "https://wa.genuka.com/en/docs/billing"
      }
    }
  ],
  "paths": {
    "/api/v1/messages": {
      "post": {
        "operationId": "sendMessage",
        "tags": [
          "Messages"
        ],
        "summary": "Send a message (template, text, media, interactive)",
        "description": "Send one WhatsApp message from a connected number (`connectionId`, from `GET /api/v1/connections`) to one recipient (`to`, international format with country code). Pass exactly one content field; one call sends one message.\n\n**Which content to use**\n- `template`: an approved message template. It is the only content that can start a conversation or reach a customer who has not written to the business in the last 24 hours (the customer service window). Use it for notifications, one-time codes (`otp` shorthand on an AUTHENTICATION template) and re-engagement. The template must be approved on the sender's WhatsApp Business Account in that `language`. Meta bills template messages to the client's own WABA; Genuka adds no markup.\n- Any other field (`text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contacts`, `reaction`, `buttons`, `list`, `cta`, `locationRequest`, `raw`): a free-form session message, delivered only inside the 24-hour window opened by the customer's last message.\n\n**The 24-hour window is reported, not enforced.** A free-form send outside it still goes to Meta, which typically accepts it and then drops it silently. The response then carries `data.warning` (`outside_service_window` or `unverified_service_window`): if you get one, resend as a template.\n\n**A 200 means Meta accepted the message, not that it was delivered.** `sent`, `delivered`, `read` and `failed` statuses arrive on your webhooks, keyed by the returned `messageId` (wamid).\n\n**Media**: upload once with `POST /api/v1/media` and pass `{ \"assetId\": \"…\" }`; `link` and a Meta `id` also work.\n\n**Refusals**: a MARKETING template to a recipient who opted out of marketing (replied STOP) is refused with 403 `recipient_opted_out`. A template Genuka knows is not approved (paused, disabled, rejected, pending) is refused with 400 `send_template`. Each accepted message counts against the plan's monthly message allowance (402 `plan_limit_messages` once exhausted); a refused send is not counted. When Meta refuses the send, `error` is `send_<errorClass>` with Meta's code and `traceId` under `meta`: `send_needs_template` means the window is closed (resend as a template), `send_recipient_permanent` means do not retry, `send_retryable` and `send_media` are worth one more try.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageSendRequest"
              },
              "examples": {
                "template": {
                  "summary": "Body-only template with positional variables",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "template": {
                      "name": "order_update",
                      "language": "en_US",
                      "body": [
                        "Alice",
                        "#1024"
                      ]
                    }
                  }
                },
                "templateRich": {
                  "summary": "Template with image header, body and URL button",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "template": {
                      "name": "order_shipped",
                      "language": "en_US",
                      "header": {
                        "image": {
                          "assetId": "ast_1"
                        }
                      },
                      "body": [
                        "Alice",
                        "#1024"
                      ],
                      "buttons": [
                        {
                          "type": "url",
                          "text": "1024"
                        }
                      ]
                    }
                  }
                },
                "otp": {
                  "summary": "One-time code (AUTHENTICATION template)",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "template": {
                      "name": "verification_code",
                      "language": "en_US",
                      "otp": "472913"
                    }
                  }
                },
                "text": {
                  "summary": "Free-form text (inside the 24-hour window)",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "text": "Thanks, your order is on its way!"
                  }
                },
                "document": {
                  "summary": "Stored document",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "document": {
                      "assetId": "ast_1",
                      "filename": "invoice-1024.pdf"
                    }
                  }
                },
                "buttons": {
                  "summary": "Interactive reply buttons",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "buttons": {
                      "body": "Confirm your order?",
                      "footer": "Acme Coffee",
                      "buttons": [
                        {
                          "id": "yes",
                          "title": "Confirm"
                        },
                        {
                          "id": "no",
                          "title": "Cancel"
                        }
                      ]
                    }
                  }
                },
                "list": {
                  "summary": "Interactive list",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "list": {
                      "body": "Pick a delivery slot",
                      "button": "Choose",
                      "sections": [
                        {
                          "title": "Today",
                          "rows": [
                            {
                              "id": "t1",
                              "title": "12:00-14:00"
                            },
                            {
                              "id": "t2",
                              "title": "14:00-16:00",
                              "description": "Most popular"
                            }
                          ]
                        }
                      ]
                    }
                  }
                },
                "cta": {
                  "summary": "Call-to-action URL button",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "cta": {
                      "body": "Your receipt is ready.",
                      "displayText": "View receipt",
                      "url": "https://example.com/r/1024"
                    }
                  }
                },
                "reply": {
                  "summary": "Reply in thread",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "replyTo": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAEhgUM0E",
                    "text": "On its way!"
                  }
                },
                "flow": {
                  "summary": "Published Flow through the raw escape hatch",
                  "value": {
                    "connectionId": "con_1",
                    "to": "+237690000001",
                    "raw": {
                      "type": "interactive",
                      "interactive": {
                        "type": "flow",
                        "body": {
                          "text": "Book your appointment in a few taps."
                        },
                        "action": {
                          "name": "flow",
                          "parameters": {
                            "flow_message_version": "3",
                            "flow_token": "booking-1024",
                            "flow_id": "1234567890123456",
                            "flow_cta": "Book now",
                            "flow_action": "navigate",
                            "flow_action_payload": {
                              "screen": "APPOINTMENT"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meta accepted the message. Delivery is reported later on webhooks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageSendResponse"
                },
                "examples": {
                  "accepted": {
                    "summary": "Accepted",
                    "value": {
                      "data": {
                        "messageId": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAERgSQjc1"
                      }
                    }
                  },
                  "outsideWindow": {
                    "summary": "Accepted, but the 24-hour window is closed",
                    "value": {
                      "data": {
                        "messageId": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAERgSQjc2",
                        "warning": {
                          "code": "outside_service_window",
                          "message": "This contact last messaged you at 2026-10-01T09:12:00.000Z, more than 24 hours ago, so the service window is closed. Meta will most likely drop this message; send an approved template instead."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, or the send was refused for a reason the payload, the template or the recipient explains. Meta usually reports a closed 24-hour window later, as a `failed` status on the webhook, rather than synchronously as `send_needs_template`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "connectionId or to missing",
                    "value": {
                      "error": "missing_fields",
                      "message": "connectionId and to are required"
                    }
                  },
                  "missing_content": {
                    "summary": "No content field",
                    "value": {
                      "error": "missing_content",
                      "message": "Provide one of: text, image, video, audio, document, sticker, location, contacts, reaction, buttons, list, cta, locationRequest, template or raw"
                    }
                  },
                  "recipient_is_sender": {
                    "summary": "Sending to the sending number itself",
                    "value": {
                      "error": "recipient_is_sender",
                      "message": "to is this connection's own number; WhatsApp cannot send a message from a number to itself"
                    }
                  },
                  "invalid_media": {
                    "summary": "Media without assetId, id or link",
                    "value": {
                      "error": "invalid_media",
                      "message": "image requires an \"assetId\" (recommended), an \"id\" or a \"link\""
                    }
                  },
                  "invalid_location": {
                    "summary": "Location without coordinates",
                    "value": {
                      "error": "invalid_location",
                      "message": "location requires latitude and longitude"
                    }
                  },
                  "invalid_button": {
                    "summary": "Unknown template button type",
                    "value": {
                      "error": "invalid_button",
                      "message": "button type must be url, quick_reply, copy_code or otp"
                    }
                  },
                  "send_needs_template": {
                    "summary": "Window closed (Meta 131047): resend as a template",
                    "value": {
                      "error": "send_needs_template",
                      "message": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
                      "meta": {
                        "errorClass": "needs_template",
                        "retryable": false,
                        "code": 131047,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  },
                  "send_template": {
                    "summary": "Template not sendable (paused, disabled, rejected or still in review)",
                    "value": {
                      "error": "send_template",
                      "message": "Template \"order_shipped\" is paused by Meta after negative recipient feedback and cannot be sent. Wait for the pause to lift or edit the template",
                      "meta": {
                        "errorClass": "template",
                        "retryable": false,
                        "code": 132015
                      }
                    }
                  },
                  "send_recipient_permanent": {
                    "summary": "Recipient unreachable (Meta 131026): do not retry",
                    "value": {
                      "error": "send_recipient_permanent",
                      "message": "Message undeliverable",
                      "meta": {
                        "errorClass": "recipient_permanent",
                        "retryable": false,
                        "code": 131026,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "description": "The client is suspended, or the recipient opted out of marketing messages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "recipient_opted_out": {
                    "summary": "MARKETING template to an opted-out recipient",
                    "value": {
                      "error": "recipient_opted_out",
                      "message": "This recipient opted out of marketing messages"
                    }
                  },
                  "account_deactivated": {
                    "summary": "Client suspended",
                    "value": {
                      "error": "account_deactivated",
                      "message": "Deactivated Account"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown connection (or outside the key's scope), or an `assetId` that does not exist for this client or passed its 30-day retention.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connectionId",
                    "value": {
                      "error": "connection_not_found"
                    }
                  },
                  "media_not_found": {
                    "summary": "Unknown or expired assetId",
                    "value": {
                      "error": "media_not_found",
                      "message": "Media asset ast_1 does not exist for this client, or has passed its retention window"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The number cannot send in its current state: released from the plan, offboarded from the WhatsApp Business app, or a Meta token/permission/registration problem (`send_config`). Reconnecting the number is the fix; retrying is not.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "number_released": {
                    "summary": "Number released from the plan",
                    "value": {
                      "error": "number_released",
                      "message": "This number (+237 6 90 00 00 01) was released from the plan and cannot send. Reconnect it through Embedded Signup — a free slot is all it needs."
                    }
                  },
                  "send_config": {
                    "summary": "Number offboarded or token/permission problem",
                    "value": {
                      "error": "send_config",
                      "message": "WhatsApp number +237 6 90 00 00 01 was offboarded by the merchant from their WhatsApp Business app — sends are suspended until it is reconnected. Data is retained.",
                      "meta": {
                        "errorClass": "config",
                        "retryable": false
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Meta was unreachable, returned no message id, or a stored file could not be uploaded to Meta. Retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "send_failed": {
                    "summary": "Meta unreachable or no message id",
                    "value": {
                      "error": "send_failed",
                      "message": "Meta did not return a message id"
                    }
                  },
                  "media_upload_failed": {
                    "summary": "Stored file could not be uploaded to Meta",
                    "value": {
                      "error": "media_upload_failed",
                      "message": "Graph API request failed with status 500"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/{id}/read": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The inbound message's wamid, as received on the `messages` webhook, URL-encoded.",
          "schema": {
            "type": "string"
          },
          "example": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAEhgUM0E"
        }
      ],
      "post": {
        "operationId": "markMessageRead",
        "tags": [
          "Messages"
        ],
        "summary": "Mark an inbound message as read, optionally with typing indicator",
        "description": "Mark an inbound message as read (blue ticks) and, with `typing: true`, show the typing indicator to the customer while you prepare a reply.\n\n`{id}` is the inbound message's wamid exactly as it arrived on the `messages` webhook, URL-encoded into the path (a wamid can contain `/` and `=`).\n\n- Marking one message read also marks every earlier message of that conversation read: call it once, on the newest inbound message.\n- Meta accepts a read receipt only within 30 days of the message; past that it refuses (422 `read_receipt_failed`) and there is nothing to retry.\n- The typing indicator disappears after 25 seconds or as soon as the business replies, whichever comes first, and cannot be cancelled or refreshed: call it when the reply is actually being produced.\n\nRead receipts do not count against the message allowance.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageReadRequest"
              },
              "examples": {
                "read": {
                  "summary": "Blue ticks only",
                  "value": {
                    "connectionId": "con_1"
                  }
                },
                "typing": {
                  "summary": "Blue ticks and typing indicator",
                  "value": {
                    "connectionId": "con_1",
                    "typing": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meta acknowledged the receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageReadResponse"
                },
                "examples": {
                  "read": {
                    "summary": "Marked read",
                    "value": {
                      "data": {
                        "success": true,
                        "messageId": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAEhgUM0E",
                        "typing": false
                      }
                    }
                  },
                  "typing": {
                    "summary": "Marked read, typing shown",
                    "value": {
                      "data": {
                        "success": true,
                        "messageId": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAEhgUM0E",
                        "typing": true,
                        "typingExpiresInSeconds": 25
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`connectionId` missing from the body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "No connectionId",
                    "value": {
                      "error": "missing_fields",
                      "message": "connectionId is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The client is suspended, or Meta refused for a token/permission reason (`read_receipt_failed` with `meta.errorClass` `config`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "account_deactivated": {
                    "summary": "Client suspended",
                    "value": {
                      "error": "account_deactivated",
                      "message": "Deactivated Account"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown connection, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connectionId",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Meta refused the receipt: typically an unknown wamid, a message from another number, or one older than 30 days. Do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "read_receipt_failed": {
                    "summary": "Refused by Meta",
                    "value": {
                      "error": "read_receipt_failed",
                      "message": "Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Meta unreachable or failing; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "read_receipt_failed": {
                    "summary": "Graph failing",
                    "value": {
                      "error": "read_receipt_failed",
                      "message": "Graph API request failed with status 500"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/media": {
      "get": {
        "operationId": "listMedia",
        "tags": [
          "Media"
        ],
        "summary": "List stored files and storage usage",
        "description": "Lists the files the key can reach, newest first, with cursor pagination, plus the account's current storage figure (`storage`) against its plan ceiling. Use it to find an `assetId` to resend, or to check remaining room before an upload that might be refused with 402 `plan_limit_media`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Only files of this kind. Any other value answers 400 `invalid_kind`.",
            "schema": {
              "$ref": "#/components/schemas/MediaKind"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, clamped to 1-100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "`nextCursor` from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of files and the storage figure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaListResponse"
                },
                "examples": {
                  "page": {
                    "summary": "One file, last page",
                    "value": {
                      "data": [
                        {
                          "id": "ast_1",
                          "companyId": "cmp_1",
                          "url": "https://wa.genuka.com/api/v1/media/ast_1/content",
                          "expiresAt": "2026-11-07T09:12:00.000Z",
                          "expiresInMs": 2591990000,
                          "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
                          "mimeType": "application/pdf",
                          "kind": "document",
                          "sizeBytes": 481203,
                          "filename": "briefing.pdf",
                          "inboundMessageId": null,
                          "createdAt": "2026-10-08T09:12:00.000Z",
                          "updatedAt": "2026-10-08T09:12:00.000Z"
                        }
                      ],
                      "storage": {
                        "usedBytes": 481203,
                        "limitBytes": 1073741824,
                        "assetCount": 1,
                        "retentionDays": 30
                      },
                      "nextCursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown `kind`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_kind": {
                    "summary": "Bad kind filter",
                    "value": {
                      "error": "invalid_kind",
                      "message": "kind must be one of image, video, audio, sticker, document"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "uploadMedia",
        "tags": [
          "Media"
        ],
        "summary": "Upload a file to send later (assetId)",
        "description": "Stores a file privately and returns its `assetId`, to pass as `{ \"assetId\": \"…\" }` in any message media object or template header at send time. Upload once, send many times, from any number of the same client.\n\n- Format and size are checked at upload against what WhatsApp accepts (see the `file` field); a refusal is a 400 `invalid_media` now rather than a failed send later.\n- Identical bytes already stored for this client are not stored twice: the existing asset comes back with 200 and `deduplicated: true` (a new file answers 201).\n- Every file is deleted 30 days after upload (`expiresAt`); a send naming an expired asset answers 404 `media_not_found`.\n- Storage counts against the plan's ceiling, shared by every client of the partner; an upload that would exceed it answers 402 `plan_limit_media` before anything is stored.\n\nThis is not how you get the `header_handle` needed to *create* a template with a media header: use `POST /api/v1/media/header-handle` for that.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/MediaUploadRequest"
              },
              "encoding": {
                "file": {
                  "contentType": "image/jpeg, image/png, image/webp, video/mp4, video/3gpp, audio/aac, audio/amr, audio/mpeg, audio/mp4, audio/ogg, text/plain, application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Identical bytes were already stored for this client; the existing asset is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaUploadResponse"
                },
                "examples": {
                  "deduplicated": {
                    "summary": "Already stored",
                    "value": {
                      "data": {
                        "id": "ast_1",
                        "companyId": "cmp_1",
                        "url": "https://wa.genuka.com/api/v1/media/ast_1/content",
                        "expiresAt": "2026-11-07T09:12:00.000Z",
                        "expiresInMs": 2591990000,
                        "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
                        "mimeType": "application/pdf",
                        "kind": "document",
                        "sizeBytes": 481203,
                        "filename": "briefing.pdf",
                        "inboundMessageId": null,
                        "createdAt": "2026-10-08T09:12:00.000Z",
                        "updatedAt": "2026-10-08T09:12:00.000Z"
                      },
                      "deduplicated": true
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaUploadResponse"
                },
                "examples": {
                  "created": {
                    "summary": "New file",
                    "value": {
                      "data": {
                        "id": "ast_1",
                        "companyId": "cmp_1",
                        "url": "https://wa.genuka.com/api/v1/media/ast_1/content",
                        "expiresAt": "2026-11-07T09:12:00.000Z",
                        "expiresInMs": 2591990000,
                        "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
                        "mimeType": "application/pdf",
                        "kind": "document",
                        "sizeBytes": 481203,
                        "filename": "briefing.pdf",
                        "inboundMessageId": null,
                        "createdAt": "2026-10-08T09:12:00.000Z",
                        "updatedAt": "2026-10-08T09:12:00.000Z"
                      },
                      "deduplicated": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not multipart, no file, no Content-Type on the file, no `companyId` with a partner-wide key, or a format/size WhatsApp does not accept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_multipart": {
                    "summary": "Body is not multipart/form-data",
                    "value": {
                      "error": "invalid_multipart",
                      "message": "Send the file as multipart/form-data"
                    }
                  },
                  "missing_file": {
                    "summary": "No `file` field",
                    "value": {
                      "error": "missing_file",
                      "message": "The `file` multipart field is required"
                    }
                  },
                  "missing_content_type": {
                    "summary": "File has no Content-Type",
                    "value": {
                      "error": "missing_content_type",
                      "message": "The uploaded file must carry a Content-Type"
                    }
                  },
                  "missing_fields": {
                    "summary": "Partner-wide key without companyId",
                    "value": {
                      "error": "missing_fields",
                      "message": "companyId is required with a partner-wide API key"
                    }
                  },
                  "invalid_media": {
                    "summary": "Type or size refused",
                    "value": {
                      "error": "invalid_media",
                      "message": "file: is 6291456 bytes; a image may not exceed 5242880"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown client, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "company_not_found": {
                    "summary": "Unknown companyId",
                    "value": {
                      "error": "company_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/media/header-handle": {
      "post": {
        "operationId": "createMediaHeaderHandle",
        "tags": [
          "Media"
        ],
        "summary": "Get a header_handle for a template with a media header",
        "description": "Uploads a sample image, video or PDF through Meta's Resumable Upload API and returns the `handle` that `POST /api/v1/templates` needs in a HEADER component with format IMAGE, VIDEO or DOCUMENT: `\"example\": { \"header_handle\": [\"<handle>\"] }`.\n\nA handle is not an `assetId` and the two are not interchangeable: the handle is consumed once, at template creation, and a send rejects it; an `assetId` is what you pass at send time (`template.header.image.assetId`). Nothing is stored on Genuka's side.\n\nAccepted: image/jpeg, image/png, video/mp4, video/3gpp, application/pdf, at most 4 MB.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/MediaHeaderHandleRequest"
              },
              "encoding": {
                "file": {
                  "contentType": "image/jpeg, image/png, video/mp4, video/3gpp, application/pdf"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Handle issued by Meta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaHeaderHandleResponse"
                },
                "examples": {
                  "handle": {
                    "summary": "PDF header sample",
                    "value": {
                      "data": {
                        "handle": "4::YXBwbGljYXRpb24vcGRm",
                        "filename": "catalogue.pdf",
                        "mimeType": "application/pdf",
                        "sizeBytes": 481203
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not multipart, no file, no Content-Type, neither `connectionId` nor `companyId` (partner-wide key), or a format a template header cannot use.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_multipart": {
                    "summary": "Body is not multipart/form-data",
                    "value": {
                      "error": "invalid_multipart",
                      "message": "Send the file as multipart/form-data"
                    }
                  },
                  "missing_file": {
                    "summary": "No `file` field",
                    "value": {
                      "error": "missing_file",
                      "message": "The `file` multipart field is required"
                    }
                  },
                  "missing_content_type": {
                    "summary": "File has no Content-Type",
                    "value": {
                      "error": "missing_content_type",
                      "message": "The uploaded file must carry a Content-Type"
                    }
                  },
                  "missing_fields": {
                    "summary": "No connectionId or companyId",
                    "value": {
                      "error": "missing_fields",
                      "message": "connectionId or companyId is required"
                    }
                  },
                  "invalid_media": {
                    "summary": "Format not allowed for a header",
                    "value": {
                      "error": "invalid_media",
                      "message": "A template header must be one of image/jpeg, image/png, video/mp4, video/3gpp, application/pdf (got image/webp)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Client suspended, `companyId` outside a client-scoped key, or Meta refused for a token/permission reason (`meta_rejected`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "account_deactivated": {
                    "summary": "Client suspended",
                    "value": {
                      "error": "account_deactivated",
                      "message": "Deactivated Account"
                    }
                  },
                  "out_of_scope": {
                    "summary": "Client-scoped key, other client",
                    "value": {
                      "error": "out_of_scope",
                      "message": "This API key is restricted to another client"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No matching connection in the key's scope (for a `companyId`, the client has no number).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "No connection",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "File above 4 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "media_too_large": {
                    "summary": "Header sample too large",
                    "value": {
                      "error": "media_too_large",
                      "message": "A template header is limited to 4194304 bytes (got 5242880)"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "description": "Media header uploads are unavailable on this deployment (no Meta app id configured). Not the caller's fault.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "app_not_configured": {
                    "summary": "Deployment misconfigured",
                    "value": {
                      "error": "app_not_configured",
                      "message": "META_APP_ID is not set on this deployment — media header uploads are unavailable"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/media/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MediaIdPath"
        }
      ],
      "get": {
        "operationId": "getMedia",
        "tags": [
          "Media"
        ],
        "summary": "Get a stored file's metadata",
        "description": "Returns one asset's metadata, including when it expires. Use `GET /api/v1/media/{id}/content` for the bytes.",
        "responses": {
          "200": {
            "description": "The asset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaAssetResponse"
                },
                "examples": {
                  "asset": {
                    "summary": "A PDF",
                    "value": {
                      "data": {
                        "id": "ast_1",
                        "companyId": "cmp_1",
                        "url": "https://wa.genuka.com/api/v1/media/ast_1/content",
                        "expiresAt": "2026-11-07T09:12:00.000Z",
                        "expiresInMs": 2591990000,
                        "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
                        "mimeType": "application/pdf",
                        "kind": "document",
                        "sizeBytes": 481203,
                        "filename": "briefing.pdf",
                        "inboundMessageId": null,
                        "createdAt": "2026-10-08T09:12:00.000Z",
                        "updatedAt": "2026-10-08T09:12:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown asset, deleted, past its 30-day retention, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "media_not_found": {
                    "summary": "Unknown asset",
                    "value": {
                      "error": "media_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteMedia",
        "tags": [
          "Media"
        ],
        "summary": "Delete a stored file now",
        "description": "Deletes the file and its Genuka record immediately, without waiting for the 30-day expiry, and gives the storage back to the plan at once. Messages already sent with it are unaffected (WhatsApp keeps its own copy on the recipient's device); future sends naming this `assetId` answer 404.",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaDeleteResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Deleted",
                    "value": {
                      "data": {
                        "id": "ast_1",
                        "deleted": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown asset, deleted, past its 30-day retention, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "media_not_found": {
                    "summary": "Unknown asset",
                    "value": {
                      "error": "media_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/media/{id}/content": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MediaIdPath"
        }
      ],
      "get": {
        "operationId": "downloadMediaContent",
        "tags": [
          "Media"
        ],
        "summary": "Download a stored file's bytes",
        "description": "Streams the file, authenticated by your API key: storage is private and this is the only way to read a file back, including files archived from inbound messages. Served as an attachment (`Content-Disposition: attachment`), never cached by shared caches, with the original `Content-Type`.",
        "responses": {
          "200": {
            "description": "The file's bytes, with the Content-Type it was uploaded with.",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"<url-encoded name>\"`.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "schema": {
                  "type": "integer"
                }
              },
              "Cache-Control": {
                "description": "`private, no-store`.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Content-Type-Options": {
                "description": "`nosniff`.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Media-Expires-At": {
                "description": "ISO 8601 date when the file is deleted for good.",
                "schema": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown asset, deleted, past its 30-day retention, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "media_not_found": {
                    "summary": "Unknown asset",
                    "value": {
                      "error": "media_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "The record exists but its bytes could not be read from storage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "media_unreadable": {
                    "summary": "Storage read failed",
                    "value": {
                      "error": "media_unreadable",
                      "message": "Media storage read failed for media/cmp_1/9f86d0.pdf"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/templates": {
      "get": {
        "operationId": "listTemplates",
        "tags": [
          "Templates"
        ],
        "summary": "List message templates",
        "description": "Lists the templates the key can reach, most recently updated first, with their review status and last known quality score. Filter by client, status, category or language. Check that a template is `approved` (and its exact `name` and `language`) before sending it or launching a campaign with it.\n\n`sync=true` first re-reads every status from Meta (same as `POST /api/v1/templates/sync`, narrowed by `companyId` or `connectionId`), which costs one Meta read per WhatsApp Business Account: use it when a template seems stuck in `pending`, not on every call.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Exact local status.",
            "schema": {
              "$ref": "#/components/schemas/TemplateStatus"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Exact category, uppercase.",
            "schema": {
              "$ref": "#/components/schemas/TemplateCategory"
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "Exact language code, e.g. `en_US`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sync",
            "in": "query",
            "required": false,
            "description": "`true` refreshes statuses from Meta before listing.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "connectionId",
            "in": "query",
            "required": false,
            "description": "With `sync=true` only: refresh just this number's WhatsApp Business Account. Does not filter the list.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Templates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One approved template",
                    "value": {
                      "data": [
                        {
                          "id": "tpl_123",
                          "companyId": "cmp_1",
                          "wabaId": "102938475610293",
                          "name": "order_shipped",
                          "language": "en_US",
                          "category": "UTILITY",
                          "components": [
                            {
                              "type": "HEADER",
                              "format": "IMAGE",
                              "example": {
                                "header_handle": [
                                  "4::aW1hZ2UvanBlZw"
                                ]
                              }
                            },
                            {
                              "type": "BODY",
                              "text": "Hi {{1}}, order {{2}} just shipped. Track it any time.",
                              "example": {
                                "body_text": [
                                  [
                                    "Alice",
                                    "#1024"
                                  ]
                                ]
                              }
                            },
                            {
                              "type": "FOOTER",
                              "text": "Reply STOP to opt out"
                            },
                            {
                              "type": "BUTTONS",
                              "buttons": [
                                {
                                  "type": "URL",
                                  "text": "Track order",
                                  "url": "https://example.com/track/{{1}}",
                                  "example": [
                                    "https://example.com/track/1024"
                                  ]
                                },
                                {
                                  "type": "QUICK_REPLY",
                                  "text": "Need help"
                                }
                              ]
                            }
                          ],
                          "status": "approved",
                          "providerId": "1253498765432109",
                          "rejectionReason": null,
                          "createdAt": "2026-10-08T09:00:00.000Z",
                          "updatedAt": "2026-10-08T09:00:00.000Z",
                          "qualityScore": {
                            "score": "GREEN",
                            "date": 1791450000
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "With `sync=true`: unknown `connectionId` or `companyId`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connection",
                    "value": {
                      "error": "connection_not_found"
                    }
                  },
                  "company_not_found": {
                    "summary": "Unknown company",
                    "value": {
                      "error": "company_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "With `sync=true`: every WhatsApp Business Account refused the read. `results` lists each one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateSyncFailure"
                },
                "examples": {
                  "allFailed": {
                    "summary": "Every account refused",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Error validating access token",
                      "results": [
                        {
                          "wabaId": "102938475610293",
                          "companyId": "cmp_1",
                          "ok": false,
                          "error": "Error validating access token"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Create a template and submit it to Meta",
        "description": "Create a message template on the client's WhatsApp Business Account and submit it to Meta for review. A template is what lets you message a customer outside the 24-hour customer service window: notifications, one-time codes, campaigns.\n\n**Two ways to create one**\n- **Custom**: pass Meta's `components` array verbatim (HEADER, BODY, FOOTER, BUTTONS, …). Every variable needs an `example`; an IMAGE, VIDEO or DOCUMENT header needs `example.header_handle` from `POST /api/v1/media/header-handle`. Genuka validates the definition before Meta sees it (400 `invalid_template`); `validate: false` skips that check.\n- **From Meta's library**: pass `libraryTemplateName` from `GET /api/v1/templates/library` (plus `libraryTemplateButtonInputs` when it has buttons, and `category: \"UTILITY\"`). Library templates skip review and are approved immediately.\n\n**Review**: the response is usually `status: \"pending\"`. The outcome arrives on your webhooks and on `GET /api/v1/templates/{id}`; `POST /api/v1/templates/sync` repairs a missed one. Only an `approved` template can be sent or used by a campaign.\n\n**AUTHENTICATION (one-time codes)**: Meta opens this category only to businesses that passed Meta business verification (or another Meta scaling path); MARKETING and UTILITY are not gated. For an unverified business Meta refuses the creation with the misleading message \"Application does not have permission for this action\" (returned as `meta_rejected`), and the account's health shows error 141010 \"The Business has not passed business verification\". Body and button texts of an authentication template are fixed by WhatsApp: you choose the OTP button type (COPY_CODE, ONE_TAP, ZERO_TAP) and options. Send codes with `POST /api/v1/messages` and `template.otp`.\n\nDefaults are `language: \"en\"` and `category: \"MARKETING\"`: pass both explicitly.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateCreateRequest"
              },
              "examples": {
                "utility": {
                  "summary": "Utility template: image header, body with variables, buttons",
                  "value": {
                    "connectionId": "con_1",
                    "name": "order_shipped",
                    "language": "en_US",
                    "category": "UTILITY",
                    "components": [
                      {
                        "type": "HEADER",
                        "format": "IMAGE",
                        "example": {
                          "header_handle": [
                            "4::aW1hZ2UvanBlZw"
                          ]
                        }
                      },
                      {
                        "type": "BODY",
                        "text": "Hi {{1}}, order {{2}} just shipped. Track it any time.",
                        "example": {
                          "body_text": [
                            [
                              "Alice",
                              "#1024"
                            ]
                          ]
                        }
                      },
                      {
                        "type": "FOOTER",
                        "text": "Reply STOP to opt out"
                      },
                      {
                        "type": "BUTTONS",
                        "buttons": [
                          {
                            "type": "URL",
                            "text": "Track order",
                            "url": "https://example.com/track/{{1}}",
                            "example": [
                              "https://example.com/track/1024"
                            ]
                          },
                          {
                            "type": "QUICK_REPLY",
                            "text": "Need help"
                          }
                        ]
                      }
                    ]
                  }
                },
                "named": {
                  "summary": "Named parameters",
                  "value": {
                    "connectionId": "con_1",
                    "name": "appointment_reminder",
                    "language": "en_US",
                    "category": "UTILITY",
                    "parameterFormat": "NAMED",
                    "components": [
                      {
                        "type": "BODY",
                        "text": "Hi {{customer_name}}, your appointment is on {{date}}.",
                        "example": {
                          "body_text_named_params": [
                            {
                              "param_name": "customer_name",
                              "example": "Alice"
                            },
                            {
                              "param_name": "date",
                              "example": "June 20"
                            }
                          ]
                        }
                      }
                    ]
                  }
                },
                "authentication": {
                  "summary": "Authentication (OTP) template, copy-code button",
                  "value": {
                    "connectionId": "con_1",
                    "name": "verification_code",
                    "language": "en_US",
                    "category": "AUTHENTICATION",
                    "messageSendTtlSeconds": 600,
                    "components": [
                      {
                        "type": "BODY",
                        "add_security_recommendation": true
                      },
                      {
                        "type": "FOOTER",
                        "code_expiration_minutes": 10
                      },
                      {
                        "type": "BUTTONS",
                        "buttons": [
                          {
                            "type": "OTP",
                            "otp_type": "COPY_CODE",
                            "text": "Copy code"
                          }
                        ]
                      }
                    ]
                  }
                },
                "library": {
                  "summary": "From Meta's library (approved immediately)",
                  "value": {
                    "connectionId": "con_1",
                    "name": "order_confirmation",
                    "language": "en_US",
                    "category": "UTILITY",
                    "libraryTemplateName": "order_confirmation_1",
                    "libraryTemplateButtonInputs": [
                      {
                        "type": "URL",
                        "url": {
                          "base_url": "https://example.com/orders/{{1}}",
                          "url_suffix_example": "https://example.com/orders/1024"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created on Meta and recorded. `status` is Meta's answer (usually `pending`; `approved` for a library template).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Submitted for review",
                    "value": {
                      "data": {
                        "id": "tpl_123",
                        "companyId": "cmp_1",
                        "wabaId": "102938475610293",
                        "name": "order_shipped",
                        "language": "en_US",
                        "category": "UTILITY",
                        "components": [
                          {
                            "type": "HEADER",
                            "format": "IMAGE",
                            "example": {
                              "header_handle": [
                                "4::aW1hZ2UvanBlZw"
                              ]
                            }
                          },
                          {
                            "type": "BODY",
                            "text": "Hi {{1}}, order {{2}} just shipped. Track it any time.",
                            "example": {
                              "body_text": [
                                [
                                  "Alice",
                                  "#1024"
                                ]
                              ]
                            }
                          },
                          {
                            "type": "FOOTER",
                            "text": "Reply STOP to opt out"
                          },
                          {
                            "type": "BUTTONS",
                            "buttons": [
                              {
                                "type": "URL",
                                "text": "Track order",
                                "url": "https://example.com/track/{{1}}",
                                "example": [
                                  "https://example.com/track/1024"
                                ]
                              },
                              {
                                "type": "QUICK_REPLY",
                                "text": "Need help"
                              }
                            ]
                          }
                        ],
                        "status": "pending",
                        "providerId": "1253498765432109",
                        "rejectionReason": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing fields, or a definition Genuka's validator refuses (fix it, or pass `validate: false` to let Meta judge).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "connectionId, name, components missing",
                    "value": {
                      "error": "missing_fields",
                      "message": "connectionId, name and components (or libraryTemplateName) are required"
                    }
                  },
                  "invalid_template": {
                    "summary": "Definition refused locally",
                    "value": {
                      "error": "invalid_template",
                      "message": "name: must contain only lowercase letters, digits and underscores"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Client suspended, or Meta refused for a permission reason, which is how an AUTHENTICATION template on an unverified business fails.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "account_deactivated": {
                    "summary": "Client suspended",
                    "value": {
                      "error": "account_deactivated",
                      "message": "Deactivated Account"
                    }
                  },
                  "meta_rejected": {
                    "summary": "AUTHENTICATION category on an unverified business",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Application does not have permission for this action",
                      "meta": {
                        "errorClass": "config",
                        "retryable": false,
                        "code": 10,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown connection, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connectionId",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A template with this name and language already exists for this client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_exists": {
                    "summary": "Duplicate name and language",
                    "value": {
                      "error": "template_exists",
                      "message": "A template with this name and language already exists"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/templates/library": {
      "get": {
        "operationId": "listTemplateLibrary",
        "tags": [
          "Templates"
        ],
        "summary": "Browse Meta's library of pre-approved templates",
        "description": "Searches Meta's library of pre-written utility templates (order updates, payment reminders, delivery issues, …). A template created from it with `POST /api/v1/templates` and `libraryTemplateName` is approved immediately, with no review: the fastest way to start sending transactional notifications. Filters are Meta's own vocabulary.",
        "parameters": [
          {
            "name": "connectionId",
            "in": "query",
            "required": false,
            "description": "Number whose access is used to query Meta. Defaults to the oldest number the key can reach.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text search.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "topic",
            "in": "query",
            "required": false,
            "description": "e.g. `ORDER_MANAGEMENT`, `PAYMENTS`, `ACCOUNT_UPDATE`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "usecase",
            "in": "query",
            "required": false,
            "description": "e.g. `PAYMENT_DUE_REMINDER`, `DELIVERY_FAILED`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "industry",
            "in": "query",
            "required": false,
            "description": "e.g. `E_COMMERCE`, `FINANCIAL_SERVICES`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "Language code, e.g. `en_US`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (Meta default 25, capped at 100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "`paging.cursors.after` from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of library templates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateLibraryResponse"
                },
                "examples": {
                  "page": {
                    "summary": "One entry",
                    "value": {
                      "data": [
                        {
                          "id": "7123456789012345",
                          "name": "order_confirmation_1",
                          "body": "Your order {{1}} is confirmed. We will let you know when it ships.",
                          "category": "UTILITY",
                          "language": [
                            "en_US"
                          ],
                          "topic": "ORDER_MANAGEMENT",
                          "usecase": "ORDER_CONFIRMATION",
                          "industry": [
                            "E_COMMERCE"
                          ],
                          "body_params": [
                            "#1024"
                          ]
                        }
                      ],
                      "paging": {
                        "cursors": {
                          "before": "QVFIUmx",
                          "after": "QVFIUnB"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The key reaches no number (or not the one named).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "No connection",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/templates/sync": {
      "post": {
        "operationId": "syncTemplates",
        "tags": [
          "Templates"
        ],
        "summary": "Re-read template statuses from Meta",
        "description": "Re-reads every template's review status, category and rejection reason from Meta and writes them back. Statuses normally arrive by webhook; a missed webhook leaves a template reading `pending` long after Meta approved it, and campaign launches refusing it. Call this before a launch, or on a schedule (hourly is plenty), not as a polling loop: each call spends one Meta read per WhatsApp Business Account.\n\nThe body is optional: empty covers every account the key can reach, `companyId` narrows to one client, `connectionId` to one number's account. Partial failure is a 200 that names the failing account in `wabas`; only a sync where every account failed is a 502.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateSyncRequest"
              },
              "examples": {
                "all": {
                  "summary": "Everything the key can reach",
                  "value": {}
                },
                "company": {
                  "summary": "One client",
                  "value": {
                    "companyId": "cmp_1"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-account outcome and the refreshed rows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateSyncResponse"
                },
                "examples": {
                  "synced": {
                    "summary": "One account refreshed",
                    "value": {
                      "data": {
                        "wabas": [
                          {
                            "wabaId": "102938475610293",
                            "companyId": "cmp_1",
                            "ok": true
                          }
                        ],
                        "synced": 1,
                        "failed": 0,
                        "templates": [
                          {
                            "id": "tpl_123",
                            "companyId": "cmp_1",
                            "wabaId": "102938475610293",
                            "name": "order_shipped",
                            "language": "en_US",
                            "category": "UTILITY",
                            "status": "approved",
                            "providerId": "1253498765432109",
                            "rejectionReason": null,
                            "updatedAt": "2026-10-08T10:00:00.000Z"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown `connectionId` or `companyId`, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connection",
                    "value": {
                      "error": "connection_not_found"
                    }
                  },
                  "company_not_found": {
                    "summary": "Unknown company",
                    "value": {
                      "error": "company_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Every account refused the read; `results` names each one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateSyncFailure"
                },
                "examples": {
                  "allFailed": {
                    "summary": "Every account refused",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Error validating access token",
                      "results": [
                        {
                          "wabaId": "102938475610293",
                          "companyId": "cmp_1",
                          "ok": false,
                          "error": "Error validating access token"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/templates/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/TemplateIdPath"
        }
      ],
      "get": {
        "operationId": "getTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Get a template with its live Meta status",
        "description": "Returns the template with its 20 most recent status changes, and asks Meta live for its current status, quality score and rejection reason; the stored status is corrected when Meta disagrees. Use it to check whether a template is approved before sending, or why it was rejected. `meta: null` means Meta could not be asked right now (the stored record is still returned), not that Meta has no data.",
        "responses": {
          "200": {
            "description": "The template.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateDetailResponse"
                },
                "examples": {
                  "approved": {
                    "summary": "Approved template",
                    "value": {
                      "data": {
                        "id": "tpl_123",
                        "companyId": "cmp_1",
                        "wabaId": "102938475610293",
                        "name": "order_shipped",
                        "language": "en_US",
                        "category": "UTILITY",
                        "components": [
                          {
                            "type": "HEADER",
                            "format": "IMAGE",
                            "example": {
                              "header_handle": [
                                "4::aW1hZ2UvanBlZw"
                              ]
                            }
                          },
                          {
                            "type": "BODY",
                            "text": "Hi {{1}}, order {{2}} just shipped. Track it any time.",
                            "example": {
                              "body_text": [
                                [
                                  "Alice",
                                  "#1024"
                                ]
                              ]
                            }
                          },
                          {
                            "type": "FOOTER",
                            "text": "Reply STOP to opt out"
                          },
                          {
                            "type": "BUTTONS",
                            "buttons": [
                              {
                                "type": "URL",
                                "text": "Track order",
                                "url": "https://example.com/track/{{1}}",
                                "example": [
                                  "https://example.com/track/1024"
                                ]
                              },
                              {
                                "type": "QUICK_REPLY",
                                "text": "Need help"
                              }
                            ]
                          }
                        ],
                        "status": "approved",
                        "providerId": "1253498765432109",
                        "rejectionReason": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z",
                        "company": {
                          "status": "active"
                        },
                        "statusEvents": [
                          {
                            "id": "tse_2",
                            "templateId": "tpl_123",
                            "fromStatus": "pending",
                            "toStatus": "approved",
                            "providerPayload": null,
                            "createdAt": "2026-10-08T09:20:00.000Z"
                          }
                        ],
                        "qualityScore": {
                          "score": "GREEN",
                          "date": 1791450000
                        },
                        "meta": {
                          "id": "1253498765432109",
                          "name": "order_shipped",
                          "language": "en_US",
                          "category": "UTILITY",
                          "status": "APPROVED",
                          "quality_score": {
                            "score": "GREEN",
                            "date": 1791450000
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown template id, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_found": {
                    "summary": "Unknown template",
                    "value": {
                      "error": "template_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Edit a template (sends it back to review)",
        "description": "Edits the template on Meta: its `components`, `category` or `messageSendTtlSeconds`. Name and language cannot change: create a new template for that.\n\nAn edit sends the template back through Meta review: its status returns to `pending`, and sends and campaign launches using it are refused until Meta approves it again. Meta also limits how often an approved template can be edited. The whole resulting definition is revalidated locally (a category change can make an existing component illegal) unless `validate: false`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateUpdateRequest"
              },
              "examples": {
                "body": {
                  "summary": "New body text",
                  "value": {
                    "components": [
                      {
                        "type": "BODY",
                        "text": "Hi {{1}}, order {{2}} is on its way.",
                        "example": {
                          "body_text": [
                            [
                              "Alice",
                              "#1024"
                            ]
                          ]
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Edited on Meta; local status is back to `pending`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateResponse"
                },
                "examples": {
                  "edited": {
                    "summary": "Back in review",
                    "value": {
                      "data": {
                        "id": "tpl_123",
                        "companyId": "cmp_1",
                        "wabaId": "102938475610293",
                        "name": "order_shipped",
                        "language": "en_US",
                        "category": "UTILITY",
                        "components": [
                          {
                            "type": "HEADER",
                            "format": "IMAGE",
                            "example": {
                              "header_handle": [
                                "4::aW1hZ2UvanBlZw"
                              ]
                            }
                          },
                          {
                            "type": "BODY",
                            "text": "Hi {{1}}, order {{2}} just shipped. Track it any time.",
                            "example": {
                              "body_text": [
                                [
                                  "Alice",
                                  "#1024"
                                ]
                              ]
                            }
                          },
                          {
                            "type": "FOOTER",
                            "text": "Reply STOP to opt out"
                          },
                          {
                            "type": "BUTTONS",
                            "buttons": [
                              {
                                "type": "URL",
                                "text": "Track order",
                                "url": "https://example.com/track/{{1}}",
                                "example": [
                                  "https://example.com/track/1024"
                                ]
                              },
                              {
                                "type": "QUICK_REPLY",
                                "text": "Need help"
                              }
                            ]
                          }
                        ],
                        "status": "pending",
                        "providerId": "1253498765432109",
                        "rejectionReason": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Nothing to change, or a definition Genuka's validator refuses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "Empty edit",
                    "value": {
                      "error": "missing_fields",
                      "message": "components, category or messageSendTtlSeconds is required"
                    }
                  },
                  "invalid_template": {
                    "summary": "Definition refused locally",
                    "value": {
                      "error": "invalid_template",
                      "message": "body.text: is required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown template, or no number of its WhatsApp Business Account within the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_found": {
                    "summary": "Unknown template",
                    "value": {
                      "error": "template_not_found"
                    }
                  },
                  "connection_not_found": {
                    "summary": "No number of the template's account in scope",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The template was never accepted by Meta (no Meta id), so there is nothing to edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_submitted": {
                    "summary": "Never accepted by Meta",
                    "value": {
                      "error": "template_not_submitted",
                      "message": "This template was never accepted by Meta"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      },
      "delete": {
        "operationId": "deleteTemplate",
        "tags": [
          "Templates"
        ],
        "summary": "Delete a template on Meta and locally",
        "description": "Deletes the template on Meta, then the Genuka record. Only this language is deleted when the template has a Meta id (`providerId`); without one, Meta deletes every language sharing the name. If Meta still holds the template after refusing the delete, the record is kept and the error returned, so a template never disappears from the list while staying sendable. If no number of its WhatsApp Business Account is within the key's scope any more, only the Genuka record is deleted.",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateDeleteResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Deleted",
                    "value": {
                      "ok": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown template id, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_found": {
                    "summary": "Unknown template",
                    "value": {
                      "error": "template_not_found"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/templates/{id}/analytics": {
      "parameters": [
        {
          "$ref": "#/components/parameters/TemplateIdPath"
        }
      ],
      "get": {
        "operationId": "getTemplateAnalytics",
        "tags": [
          "Templates"
        ],
        "summary": "Get a template's daily sent/delivered/read/clicked figures",
        "description": "Daily figures for one template: sent, delivered, read, clicked (with a per-button breakdown) and Meta's cost metric. Each call refreshes Genuka's archive from Meta for the last 90 days of the window, then answers from the archive. That matters because Meta purges read and click counters after 7 days and keeps nothing past a year: the archive keeps what Meta drops.\n\nAnalytics are off by default on a WhatsApp Business Account and are never collected retroactively. If everything is empty and `meta.warning` says Meta returned no data points, call `POST` on this same route once to enable them. A failed live refresh is not an error: the archive is served and `meta.warning` says so.",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "Window start: ISO 8601 date/time or Unix seconds. Defaults to 30 days before `end`; an unparseable value falls back to that default.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-08"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "Window end: ISO 8601 date/time or Unix seconds. Defaults to now; an unparseable value falls back to now.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-08"
          }
        ],
        "responses": {
          "200": {
            "description": "Daily rows and window metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateAnalyticsResponse"
                },
                "examples": {
                  "days": {
                    "summary": "One day",
                    "value": {
                      "data": [
                        {
                          "day": "2026-10-07",
                          "template": "order_shipped",
                          "templateId": "1253498765432109",
                          "sent": 120,
                          "delivered": 117,
                          "read": 98,
                          "clicked": 31,
                          "cost": 0,
                          "clicksByButton": [
                            {
                              "type": "url_button",
                              "button_content": "Track order",
                              "count": 31
                            }
                          ]
                        }
                      ],
                      "meta": {
                        "template": "order_shipped",
                        "granularity": "DAILY",
                        "start": "2026-09-08T00:00:00.000Z",
                        "end": "2026-10-08T00:00:00.000Z",
                        "metaClickRetentionDays": 7,
                        "warning": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`start` is not before `end`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_range": {
                    "summary": "Bad window",
                    "value": {
                      "error": "invalid_range",
                      "message": "start must be before end"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown template id, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_found": {
                    "summary": "Unknown template",
                    "value": {
                      "error": "template_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The template was never accepted by Meta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_submitted": {
                    "summary": "Never accepted by Meta",
                    "value": {
                      "error": "template_not_submitted",
                      "message": "This template was never accepted by Meta"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "enableTemplateAnalytics",
        "tags": [
          "Templates"
        ],
        "summary": "Enable template analytics for the template's account",
        "description": "Switches template analytics on for the whole WhatsApp Business Account that owns this template (one call per account, not per template). Off by default and never retroactive: enable it as soon as a number is connected. No request body.",
        "responses": {
          "200": {
            "description": "Enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateAnalyticsEnableResponse"
                },
                "examples": {
                  "enabled": {
                    "summary": "Enabled",
                    "value": {
                      "ok": true,
                      "enabled": true,
                      "wabaId": "102938475610293"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown template, or no number of its account within the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_found": {
                    "summary": "Unknown template",
                    "value": {
                      "error": "template_not_found"
                    }
                  },
                  "connection_not_found": {
                    "summary": "No number of the template's account in scope",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/flows": {
      "get": {
        "operationId": "listFlows",
        "tags": [
          "Flows"
        ],
        "summary": "List WhatsApp Flows",
        "description": "Lists the Flows the key can reach (forms and multi-screen experiences shown inside WhatsApp), most recently updated first. The Flow JSON is not included; use `GET /api/v1/flows/{id}`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "Flows.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One draft",
                    "value": {
                      "data": [
                        {
                          "id": "flw_1",
                          "companyId": "cmp_1",
                          "connectionId": "con_1",
                          "providerId": "1234567890123456",
                          "name": "appointment_booking",
                          "categories": [
                            "APPOINTMENT_BOOKING"
                          ],
                          "status": "draft",
                          "version": "6.2",
                          "endpointUri": null,
                          "publishedAt": null,
                          "createdAt": "2026-10-08T09:00:00.000Z",
                          "updatedAt": "2026-10-08T09:00:00.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createFlow",
        "tags": [
          "Flows"
        ],
        "summary": "Create a WhatsApp Flow",
        "description": "Creates a Flow on the client's WhatsApp Business Account (status `draft`), optionally with its Flow JSON, and records it. The Flow JSON is validated by Genuka before Meta sees it (422 `invalid_flow`); Meta then runs its own checks and returns `validationErrors`, which do not fail the call: read them, fix the JSON with PATCH, then publish.\n\nA draft can be edited, previewed (`GET /api/v1/flows/{id}/preview`) and deleted. Publishing (`POST /api/v1/flows/{id}/publish`, or `publish: true` here) is one-way. To show a published Flow to a customer inside the 24-hour window, send `POST /api/v1/messages` with `raw` set to an interactive message of type `flow` whose `flow_id` is the Flow's `providerId`; outside the window, send an approved template that has a FLOW button.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FlowCreateRequest"
              },
              "examples": {
                "withJson": {
                  "summary": "Draft with its Flow JSON",
                  "value": {
                    "connectionId": "con_1",
                    "name": "appointment_booking",
                    "categories": [
                      "APPOINTMENT_BOOKING"
                    ],
                    "flowJson": {
                      "version": "6.2",
                      "screens": [
                        {
                          "id": "APPOINTMENT",
                          "title": "Book an appointment",
                          "terminal": true,
                          "layout": {
                            "type": "SingleColumnLayout",
                            "children": [
                              {
                                "type": "Form",
                                "name": "booking",
                                "children": [
                                  {
                                    "type": "DatePicker",
                                    "name": "date",
                                    "label": "Date",
                                    "required": true
                                  },
                                  {
                                    "type": "Footer",
                                    "label": "Confirm",
                                    "on-click-action": {
                                      "name": "complete",
                                      "payload": {
                                        "date": "${form.date}"
                                      }
                                    }
                                  }
                                ]
                              }
                            ]
                          }
                        }
                      ]
                    }
                  }
                },
                "empty": {
                  "summary": "Empty draft, filled later",
                  "value": {
                    "connectionId": "con_1",
                    "name": "lead_form",
                    "categories": [
                      "LEAD_GENERATION"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created on Meta and recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Draft created",
                    "value": {
                      "data": {
                        "id": "flw_1",
                        "companyId": "cmp_1",
                        "connectionId": "con_1",
                        "providerId": "1234567890123456",
                        "name": "appointment_booking",
                        "categories": [
                          "APPOINTMENT_BOOKING"
                        ],
                        "status": "draft",
                        "flowJson": {
                          "version": "6.2",
                          "screens": [
                            {
                              "id": "APPOINTMENT",
                              "title": "Book an appointment",
                              "terminal": true,
                              "layout": {
                                "type": "SingleColumnLayout",
                                "children": [
                                  {
                                    "type": "Form",
                                    "name": "booking",
                                    "children": [
                                      {
                                        "type": "DatePicker",
                                        "name": "date",
                                        "label": "Date",
                                        "required": true
                                      },
                                      {
                                        "type": "Footer",
                                        "label": "Confirm",
                                        "on-click-action": {
                                          "name": "complete",
                                          "payload": {
                                            "date": "${form.date}"
                                          }
                                        }
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          ]
                        },
                        "version": "6.2",
                        "endpointUri": null,
                        "publishedAt": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z"
                      },
                      "validationErrors": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`connectionId`, `name` or `categories` missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "Missing fields",
                    "value": {
                      "error": "missing_fields",
                      "message": "connectionId, name and categories are required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown connection, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connectionId",
                    "value": {
                      "error": "connection_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A Flow with this name already exists for this client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_exists": {
                    "summary": "Duplicate name",
                    "value": {
                      "error": "flow_exists",
                      "message": "A flow with this name already exists for this company"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The Flow JSON, categories, endpoint URI or lifecycle transition is invalid (checked by Genuka before Meta), or Meta refused the payload (`meta_rejected`, with `meta`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_flow": {
                    "summary": "Refused by the validator",
                    "value": {
                      "error": "invalid_flow",
                      "message": "flow_json.screens[0].id: must match /^[A-Za-z0-9_]+$/ (got \"appointment-1\")"
                    }
                  },
                  "meta_rejected": {
                    "summary": "Refused by Meta",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/flows/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/FlowIdPath"
        }
      ],
      "get": {
        "operationId": "getFlow",
        "tags": [
          "Flows"
        ],
        "summary": "Get a Flow with its live Meta status",
        "description": "Returns the Flow and, when it exists on Meta, refreshes its status from Meta (Meta can block or throttle a Flow on its own) and returns Meta's current `validationErrors`. If Meta cannot be reached, the stored record is returned without `validationErrors`.",
        "responses": {
          "200": {
            "description": "The Flow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowResponse"
                },
                "examples": {
                  "draft": {
                    "summary": "A draft",
                    "value": {
                      "data": {
                        "id": "flw_1",
                        "companyId": "cmp_1",
                        "connectionId": "con_1",
                        "providerId": "1234567890123456",
                        "name": "appointment_booking",
                        "categories": [
                          "APPOINTMENT_BOOKING"
                        ],
                        "status": "draft",
                        "flowJson": {
                          "version": "6.2",
                          "screens": [
                            {
                              "id": "APPOINTMENT",
                              "title": "Book an appointment",
                              "terminal": true,
                              "layout": {
                                "type": "SingleColumnLayout",
                                "children": [
                                  {
                                    "type": "Form",
                                    "name": "booking",
                                    "children": [
                                      {
                                        "type": "DatePicker",
                                        "name": "date",
                                        "label": "Date",
                                        "required": true
                                      },
                                      {
                                        "type": "Footer",
                                        "label": "Confirm",
                                        "on-click-action": {
                                          "name": "complete",
                                          "payload": {
                                            "date": "${form.date}"
                                          }
                                        }
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          ]
                        },
                        "version": "6.2",
                        "endpointUri": null,
                        "publishedAt": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z"
                      },
                      "validationErrors": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown Flow, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_not_found": {
                    "summary": "Unknown Flow",
                    "value": {
                      "error": "flow_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateFlow",
        "tags": [
          "Flows"
        ],
        "summary": "Update a Flow's metadata or JSON, or deprecate it",
        "description": "Changes the Flow's name, categories, endpoint URI and/or replaces its Flow JSON on Meta, or deprecates a published Flow with `status: \"deprecated\"` (a published Flow cannot be deleted). Publishing has its own route. The Flow must exist on Meta and be attached to a number.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FlowUpdateRequest"
              },
              "examples": {
                "json": {
                  "summary": "Replace the Flow JSON",
                  "value": {
                    "flowJson": {
                      "version": "6.2",
                      "screens": [
                        {
                          "id": "APPOINTMENT",
                          "title": "Book an appointment",
                          "terminal": true,
                          "layout": {
                            "type": "SingleColumnLayout",
                            "children": [
                              {
                                "type": "Form",
                                "name": "booking",
                                "children": [
                                  {
                                    "type": "DatePicker",
                                    "name": "date",
                                    "label": "Date",
                                    "required": true
                                  },
                                  {
                                    "type": "Footer",
                                    "label": "Confirm",
                                    "on-click-action": {
                                      "name": "complete",
                                      "payload": {
                                        "date": "${form.date}"
                                      }
                                    }
                                  }
                                ]
                              }
                            ]
                          }
                        }
                      ]
                    }
                  }
                },
                "deprecate": {
                  "summary": "Retire a published Flow",
                  "value": {
                    "status": "deprecated"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "Updated draft",
                    "value": {
                      "data": {
                        "id": "flw_1",
                        "companyId": "cmp_1",
                        "connectionId": "con_1",
                        "providerId": "1234567890123456",
                        "name": "appointment_booking",
                        "categories": [
                          "APPOINTMENT_BOOKING"
                        ],
                        "status": "draft",
                        "flowJson": {
                          "version": "6.2",
                          "screens": [
                            {
                              "id": "APPOINTMENT",
                              "title": "Book an appointment",
                              "terminal": true,
                              "layout": {
                                "type": "SingleColumnLayout",
                                "children": [
                                  {
                                    "type": "Form",
                                    "name": "booking",
                                    "children": [
                                      {
                                        "type": "DatePicker",
                                        "name": "date",
                                        "label": "Date",
                                        "required": true
                                      },
                                      {
                                        "type": "Footer",
                                        "label": "Confirm",
                                        "on-click-action": {
                                          "name": "complete",
                                          "payload": {
                                            "date": "${form.date}"
                                          }
                                        }
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          ]
                        },
                        "version": "6.2",
                        "endpointUri": null,
                        "publishedAt": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Empty body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "Nothing to update",
                    "value": {
                      "error": "missing_fields",
                      "message": "Provide at least one field to update"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown Flow, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_not_found": {
                    "summary": "Unknown Flow",
                    "value": {
                      "error": "flow_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The Flow was never created on Meta or is not attached to a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_required": {
                    "summary": "Flow not attached to a number",
                    "value": {
                      "error": "connection_required",
                      "message": "This flow is not attached to a WhatsApp connection"
                    }
                  },
                  "flow_not_on_meta": {
                    "summary": "Flow has no Meta id",
                    "value": {
                      "error": "flow_not_on_meta",
                      "message": "This flow has no Meta id yet"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Invalid Flow JSON, metadata or transition (e.g. deprecating a Flow that is not published), a status other than `deprecated`, or a payload Meta refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_flow": {
                    "summary": "Refused by the validator",
                    "value": {
                      "error": "invalid_flow",
                      "message": "flow.status: deprecate requires status PUBLISHED — flow 1234567890123456 is DRAFT"
                    }
                  },
                  "unsupported_transition": {
                    "summary": "Only deprecated is accepted",
                    "value": {
                      "error": "unsupported_transition",
                      "message": "Only `deprecated` can be set here; use POST /flows/{id}/publish to publish"
                    }
                  },
                  "meta_rejected": {
                    "summary": "Refused by Meta",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      },
      "delete": {
        "operationId": "deleteFlow",
        "tags": [
          "Flows"
        ],
        "summary": "Delete a draft Flow",
        "description": "Deletes a draft Flow on Meta (when it was created there) and locally. Only drafts can be deleted: a published Flow is part of conversations that already happened and can only be deprecated (PATCH with `status: \"deprecated\"`).",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowDeleteResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Deleted",
                    "value": {
                      "ok": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown Flow, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_not_found": {
                    "summary": "Unknown Flow",
                    "value": {
                      "error": "flow_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The Flow is not a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_not_draft": {
                    "summary": "Not a draft",
                    "value": {
                      "error": "flow_not_draft",
                      "message": "A published flow cannot be deleted — deprecate it instead (PATCH with status: \"deprecated\")"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The Flow JSON, categories, endpoint URI or lifecycle transition is invalid (checked by Genuka before Meta), or Meta refused the payload (`meta_rejected`, with `meta`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_flow": {
                    "summary": "Refused by the validator",
                    "value": {
                      "error": "invalid_flow",
                      "message": "flow_json.screens[0].id: must match /^[A-Za-z0-9_]+$/ (got \"appointment-1\")"
                    }
                  },
                  "meta_rejected": {
                    "summary": "Refused by Meta",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/flows/{id}/preview": {
      "parameters": [
        {
          "$ref": "#/components/parameters/FlowIdPath"
        }
      ],
      "get": {
        "operationId": "getFlowPreview",
        "tags": [
          "Flows"
        ],
        "summary": "Get a preview link for a Flow",
        "description": "Returns Meta's preview URL for the Flow, to open or embed in an iframe before publishing. The link is stable and expires after 30 days; `invalidate=true` mints a new one and kills the previous link immediately (the only way to revoke a preview shared too widely).",
        "parameters": [
          {
            "name": "invalidate",
            "in": "query",
            "required": false,
            "description": "`true` revokes the current link and returns a new one.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Preview link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowPreviewResponse"
                },
                "examples": {
                  "preview": {
                    "summary": "Preview link",
                    "value": {
                      "data": {
                        "url": "https://business.facebook.com/wa/manage/flows/1234567890123456/preview/?token=abc",
                        "expiresAt": "2026-11-07T09:00:00+0000"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown Flow, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_not_found": {
                    "summary": "Unknown Flow",
                    "value": {
                      "error": "flow_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The Flow was never created on Meta or is not attached to a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_required": {
                    "summary": "Flow not attached to a number",
                    "value": {
                      "error": "connection_required",
                      "message": "This flow is not attached to a WhatsApp connection"
                    }
                  },
                  "flow_not_on_meta": {
                    "summary": "Flow has no Meta id",
                    "value": {
                      "error": "flow_not_on_meta",
                      "message": "This flow has no Meta id yet"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/MetaRejected"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/flows/{id}/publish": {
      "parameters": [
        {
          "$ref": "#/components/parameters/FlowIdPath"
        }
      ],
      "post": {
        "operationId": "publishFlow",
        "tags": [
          "Flows"
        ],
        "summary": "Publish a draft Flow",
        "description": "Publishes a draft Flow so it can be sent to customers. One-way: afterwards the Flow can only be deprecated, never edited back to draft or deleted. Meta re-runs its own validation on publish: `validationErrors` are returned verbatim (each with the JSON location to fix), and the returned `status` is read back from Meta rather than assumed. No request body. To show a published Flow to a customer inside the 24-hour window, send `POST /api/v1/messages` with `raw` set to an interactive message of type `flow` whose `flow_id` is the Flow's `providerId`; outside the window, send an approved template that has a FLOW button.",
        "responses": {
          "200": {
            "description": "Published (check `data.status` and `validationErrors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowResponse"
                },
                "examples": {
                  "published": {
                    "summary": "Published",
                    "value": {
                      "data": {
                        "id": "flw_1",
                        "companyId": "cmp_1",
                        "connectionId": "con_1",
                        "providerId": "1234567890123456",
                        "name": "appointment_booking",
                        "categories": [
                          "APPOINTMENT_BOOKING"
                        ],
                        "status": "published",
                        "flowJson": {
                          "version": "6.2",
                          "screens": [
                            {
                              "id": "APPOINTMENT",
                              "title": "Book an appointment",
                              "terminal": true,
                              "layout": {
                                "type": "SingleColumnLayout",
                                "children": [
                                  {
                                    "type": "Form",
                                    "name": "booking",
                                    "children": [
                                      {
                                        "type": "DatePicker",
                                        "name": "date",
                                        "label": "Date",
                                        "required": true
                                      },
                                      {
                                        "type": "Footer",
                                        "label": "Confirm",
                                        "on-click-action": {
                                          "name": "complete",
                                          "payload": {
                                            "date": "${form.date}"
                                          }
                                        }
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          ]
                        },
                        "version": "6.2",
                        "endpointUri": null,
                        "publishedAt": "2026-10-08T10:00:00.000Z",
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z"
                      },
                      "validationErrors": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown Flow, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_not_found": {
                    "summary": "Unknown Flow",
                    "value": {
                      "error": "flow_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already published, not a draft, never created on Meta, or not attached to a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "flow_already_published": {
                    "summary": "Already published",
                    "value": {
                      "error": "flow_already_published"
                    }
                  },
                  "flow_not_draft": {
                    "summary": "Deprecated, blocked or throttled",
                    "value": {
                      "error": "flow_not_draft",
                      "message": "A deprecated flow cannot be published"
                    }
                  },
                  "connection_required": {
                    "summary": "Flow not attached to a number",
                    "value": {
                      "error": "connection_required",
                      "message": "This flow is not attached to a WhatsApp connection"
                    }
                  },
                  "flow_not_on_meta": {
                    "summary": "Flow has no Meta id",
                    "value": {
                      "error": "flow_not_on_meta",
                      "message": "This flow has no Meta id yet"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The Flow JSON, categories, endpoint URI or lifecycle transition is invalid (checked by Genuka before Meta), or Meta refused the payload (`meta_rejected`, with `meta`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_flow": {
                    "summary": "Refused by the validator",
                    "value": {
                      "error": "invalid_flow",
                      "message": "flow_json.screens[0].id: must match /^[A-Za-z0-9_]+$/ (got \"appointment-1\")"
                    }
                  },
                  "meta_rejected": {
                    "summary": "Refused by Meta",
                    "value": {
                      "error": "meta_rejected",
                      "message": "Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbCdEfGh123"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/campaigns": {
      "get": {
        "operationId": "listCampaigns",
        "tags": [
          "Campaigns"
        ],
        "summary": "List campaigns",
        "description": "Lists the campaigns the key can reach, newest first, with their recipient count.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "Campaigns.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One campaign",
                    "value": {
                      "data": [
                        {
                          "id": "cpg_9",
                          "companyId": "cmp_1",
                          "connectionId": "con_1",
                          "templateId": "tpl_124",
                          "name": "October promo",
                          "status": "completed",
                          "createdAt": "2026-10-08T09:00:00.000Z",
                          "_count": {
                            "recipients": 2
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createCampaign",
        "tags": [
          "Campaigns"
        ],
        "summary": "Create a campaign (template + recipients)",
        "description": "Creates a draft campaign: one template sent from one number to a list of recipients, each with its own variables. Nothing is sent until `POST /api/v1/campaigns/{id}/launch`. The template and the number must belong to the same client; the template need not be approved yet, but must be by launch time. Recipients with an empty `to` are dropped silently (the response's `_count.recipients` tells how many were kept).\n\nUse a campaign to send the same template to many people (promotions, reminders, announcements). For a single notification or one-time code, `POST /api/v1/messages` with `template` is simpler.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CampaignCreateRequest"
              },
              "examples": {
                "positional": {
                  "summary": "Positional body variables",
                  "value": {
                    "connectionId": "con_1",
                    "templateId": "tpl_124",
                    "name": "October promo",
                    "recipients": [
                      {
                        "to": "+237690000001",
                        "variables": [
                          "Alice",
                          "#1024"
                        ]
                      },
                      {
                        "to": "+237690000002",
                        "variables": [
                          "Bob",
                          "#1025"
                        ]
                      }
                    ]
                  }
                },
                "rich": {
                  "summary": "Image header and URL button per recipient",
                  "value": {
                    "connectionId": "con_1",
                    "templateId": "tpl_123",
                    "name": "Shipping updates",
                    "recipients": [
                      {
                        "to": "+237690000001",
                        "variables": {
                          "header": {
                            "image": {
                              "assetId": "ast_1"
                            }
                          },
                          "body": [
                            "Alice",
                            "#1024"
                          ],
                          "buttons": [
                            {
                              "type": "url",
                              "text": "1024"
                            }
                          ]
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignCreateResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Draft with two recipients",
                    "value": {
                      "data": {
                        "id": "cpg_9",
                        "name": "October promo",
                        "status": "draft",
                        "_count": {
                          "recipients": 2
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing fields, no usable recipient, or template and number of different clients.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_fields": {
                    "summary": "Missing fields",
                    "value": {
                      "error": "missing_fields",
                      "message": "connectionId, templateId, name and a non-empty recipients[] are required"
                    }
                  },
                  "no_valid_recipients": {
                    "summary": "Every recipient had an empty `to`",
                    "value": {
                      "error": "no_valid_recipients"
                    }
                  },
                  "template_connection_mismatch": {
                    "summary": "Template and number belong to different clients",
                    "value": {
                      "error": "template_connection_mismatch",
                      "message": "Template and connection belong to different clients"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown connection or template, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "connection_not_found": {
                    "summary": "Unknown connectionId",
                    "value": {
                      "error": "connection_not_found"
                    }
                  },
                  "template_not_found": {
                    "summary": "Unknown templateId",
                    "value": {
                      "error": "template_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CampaignIdPath"
        }
      ],
      "get": {
        "operationId": "getCampaign",
        "tags": [
          "Campaigns"
        ],
        "summary": "Get a campaign with delivery stats",
        "description": "Returns the campaign, its template and sending number, and `stats`: the number of recipients in each status. Recipient statuses advance as Meta's status webhooks arrive, so re-read it to follow delivery after a launch.",
        "responses": {
          "200": {
            "description": "The campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignDetailResponse"
                },
                "examples": {
                  "completed": {
                    "summary": "After a launch",
                    "value": {
                      "data": {
                        "id": "cpg_9",
                        "companyId": "cmp_1",
                        "connectionId": "con_1",
                        "templateId": "tpl_124",
                        "name": "October promo",
                        "messageType": "template",
                        "status": "completed",
                        "scheduledAt": null,
                        "costEstimate": "0",
                        "metadata": null,
                        "createdAt": "2026-10-08T09:00:00.000Z",
                        "updatedAt": "2026-10-08T09:05:00.000Z",
                        "company": {
                          "status": "active"
                        },
                        "template": {
                          "id": "tpl_124",
                          "name": "order_update",
                          "language": "en_US",
                          "status": "approved"
                        },
                        "connection": {
                          "id": "con_1",
                          "displayPhoneNumber": "+237 6 90 00 00 00",
                          "phoneNumberId": "106540352242922"
                        },
                        "_count": {
                          "recipients": 2
                        },
                        "stats": {
                          "delivered": 1,
                          "read": 1
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown campaign, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "campaign_not_found": {
                    "summary": "Unknown campaign",
                    "value": {
                      "error": "campaign_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}/recipients": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CampaignIdPath"
        }
      ],
      "get": {
        "operationId": "listCampaignRecipients",
        "tags": [
          "Campaigns"
        ],
        "summary": "List a campaign's recipients and their delivery status",
        "description": "Per-recipient status, wamid, timestamps and failure reason, in creation order. Filter by `status` to find who failed or was skipped. There is no cursor: `limit` caps the result (default 200, max 1000).",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only recipients in this status.",
            "schema": {
              "$ref": "#/components/schemas/CampaignRecipientStatus"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows returned (capped at 1000).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recipients.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignRecipientListResponse"
                },
                "examples": {
                  "recipients": {
                    "summary": "One delivered, one failed",
                    "value": {
                      "data": [
                        {
                          "id": "rcp_1",
                          "contact": "+237690000001",
                          "status": "delivered",
                          "providerMessageId": "wamid.HBgMMjM3NjkwMDAwMDAxFQIAERgSQjc1",
                          "costCredits": 0,
                          "errorMessage": null,
                          "sentAt": "2026-10-08T09:01:00.000Z",
                          "deliveredAt": "2026-10-08T09:01:03.000Z",
                          "readAt": null,
                          "failedAt": null
                        },
                        {
                          "id": "rcp_2",
                          "contact": "+237690000002",
                          "status": "failed",
                          "providerMessageId": null,
                          "costCredits": 0,
                          "errorMessage": "Message undeliverable",
                          "sentAt": null,
                          "deliveredAt": null,
                          "readAt": null,
                          "failedAt": "2026-10-08T09:01:01.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown campaign, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "campaign_not_found": {
                    "summary": "Unknown campaign",
                    "value": {
                      "error": "campaign_not_found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/campaigns/{id}/launch": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CampaignIdPath"
        }
      ],
      "post": {
        "operationId": "launchCampaign",
        "tags": [
          "Campaigns"
        ],
        "summary": "Launch a campaign (send to pending recipients)",
        "description": "Sends the campaign's template to every recipient still `pending`, then returns the counts. This is the bulk-send primitive: Genuka WA has no campaign console, a campaign is driven entirely through these endpoints.\n\n**Synchronous**: the call returns when every pending recipient has been attempted, one recipient after another, paced to stay under the number's throughput ceiling (80 messages per second by default, 1000 on numbers Meta upgraded, 20 on a number shared with the WhatsApp Business app). Keep lists modest per call, and set a long client timeout.\n\n**Preconditions**: the template must be `approved` (400 `send_template` names the reason otherwise: paused, disabled, rejected or still in review; refresh with `POST /api/v1/templates/sync` if Meta approved it and the webhook was missed); the number must still be connected and hold a plan slot; and the whole pending list must fit in the plan's remaining message allowance, checked before the first send (402 `plan_limit_messages`, nothing sent).\n\n**What happens to recipients**: with a MARKETING template, recipients who opted out of marketing are marked `skipped`. Each recipient is retried on transient errors; one Meta refused is marked `failed` with `errorMessage`; one Meta throttled (per-user marketing cap) stays `pending`. Launching again after the previous call returned only reaches recipients still `pending`, which is also how to resume a launch cut off by a timeout. There is no lock: never run two launches of the same campaign at once, both would send to the same pending recipients. Only accepted messages count against the allowance. Delivery and read statuses then arrive on webhooks and in `GET /api/v1/campaigns/{id}/recipients`. Meta bills each template message to the client's WABA.",
        "responses": {
          "200": {
            "description": "Every pending recipient was attempted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignLaunchResponse"
                },
                "examples": {
                  "launched": {
                    "summary": "Launched",
                    "value": {
                      "data": {
                        "sent": 2,
                        "failed": 0,
                        "skipped": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The campaign has no template, or its template cannot be sent (not approved).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "campaign_has_no_template": {
                    "summary": "No template",
                    "value": {
                      "error": "campaign_has_no_template"
                    }
                  },
                  "send_template": {
                    "summary": "Template not approved",
                    "value": {
                      "error": "send_template",
                      "message": "Template \"order_shipped\" is still under review by Meta",
                      "meta": {
                        "errorClass": "template",
                        "retryable": false
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Unknown campaign, or outside the key's scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "campaign_not_found": {
                    "summary": "Unknown campaign",
                    "value": {
                      "error": "campaign_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The sending number was released from the plan, or offboarded from the WhatsApp Business app (`send_config`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "number_released": {
                    "summary": "Number released from the plan",
                    "value": {
                      "error": "number_released",
                      "message": "This number (+237 6 90 00 00 00) was released from the plan and cannot send. Reconnect it through Embedded Signup — a free slot is all it needs."
                    }
                  },
                  "send_config": {
                    "summary": "Number offboarded",
                    "value": {
                      "error": "send_config",
                      "message": "WhatsApp number +237 6 90 00 00 00 was offboarded by the merchant from their WhatsApp Business app — sends are suspended until it is reconnected. Data is retained.",
                      "meta": {
                        "errorClass": "config",
                        "retryable": false
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/numbers": {
      "get": {
        "operationId": "listNumbers",
        "tags": [
          "Numbers"
        ],
        "summary": "List WhatsApp numbers with their health",
        "description": "Lists every WhatsApp number (connection) the API key can reach, with the fields that decide whether and how fast it can send: `qualityRating`, Meta's `messagingLimitTier`, `throughputLevel`, `platformType` and the derived `messagesPerSecond`.\n\nWhen to use: to pick a `connectionId` before a send, to size a campaign (the tier caps how many unique customers can receive template messages in a rolling 24 hours, and `messagesPerSecond` sets how long a batch takes), or to spot coexistence numbers (`coexistence: true`): numbers shared with the WhatsApp Business app, fixed at 20 messages per second.\n\nBy default the values come from Genuka's database (`source: \"database\"`) as stored by the last health read: instant, and costs no Meta call. `refresh=true` reads Meta live for each number, stores the result and adds `metaStatus`, `refreshed`, and `alerts` (what changed) or `refreshError` per number; one unreachable number does not fail the list. Refresh is capped at 25 numbers per call (`400 too_many_connections` above that): narrow with `companyId`, or use `GET /api/v1/numbers/{id}/health` per number.\n\nA number with `releasedAt` set was released from the plan and refuses sends with `409 number_released` until it is reconnected through Embedded Signup. No pagination: every number is returned, main numbers first.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          },
          {
            "name": "refresh",
            "in": "query",
            "required": false,
            "description": "`true` reads Meta live for each number (at most 25). Any other value reads the database.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The numbers, from the database or freshly read from Meta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NumberListResponse"
                },
                "examples": {
                  "database": {
                    "summary": "Default (stored values)",
                    "value": {
                      "data": [
                        {
                          "id": "cm8x4k9r20003qz7hb2c6e8f4",
                          "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                          "wabaId": "102290129340398",
                          "phoneNumberId": "106540352242922",
                          "displayPhoneNumber": "+237 6 99 00 11 22",
                          "verifiedName": "Acme Coffee",
                          "qualityRating": "GREEN",
                          "messagingLimitTier": "TIER_1K",
                          "throughputLevel": "STANDARD",
                          "platformType": "CLOUD_API",
                          "status": "connected",
                          "isMain": true,
                          "offboardedAt": null,
                          "releasedAt": null,
                          "coexistence": false,
                          "messagesPerSecond": 80
                        },
                        {
                          "id": "cm8x5a1c70011qz7h3k9m2p6t",
                          "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                          "wabaId": "102290129340411",
                          "phoneNumberId": "106540352242987",
                          "displayPhoneNumber": "+237 6 77 88 99 00",
                          "verifiedName": "Acme Coffee Bonamoussadi",
                          "qualityRating": "UNKNOWN",
                          "messagingLimitTier": "TIER_250",
                          "throughputLevel": "STANDARD",
                          "platformType": "SMB_APP",
                          "status": "connected",
                          "isMain": false,
                          "offboardedAt": null,
                          "releasedAt": null,
                          "coexistence": true,
                          "messagesPerSecond": 20
                        }
                      ],
                      "source": "database"
                    }
                  },
                  "refreshed": {
                    "summary": "refresh=true",
                    "value": {
                      "data": [
                        {
                          "id": "cm8x4k9r20003qz7hb2c6e8f4",
                          "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                          "wabaId": "102290129340398",
                          "phoneNumberId": "106540352242922",
                          "displayPhoneNumber": "+237 6 99 00 11 22",
                          "verifiedName": "Acme Coffee",
                          "qualityRating": "YELLOW",
                          "messagingLimitTier": "TIER_1K",
                          "throughputLevel": "STANDARD",
                          "platformType": "CLOUD_API",
                          "status": "connected",
                          "isMain": true,
                          "offboardedAt": null,
                          "releasedAt": null,
                          "coexistence": false,
                          "messagesPerSecond": 80,
                          "metaStatus": "CONNECTED",
                          "refreshed": true,
                          "alerts": [
                            {
                              "kind": "quality_dropped",
                              "severity": "warning",
                              "from": "GREEN",
                              "to": "YELLOW",
                              "message": "Quality fell from GREEN to YELLOW. This is the warning shot before a restriction — review recent template sends and opt-out handling."
                            }
                          ]
                        }
                      ],
                      "source": "meta",
                      "limits": {
                        "throughput": {
                          "default": 80,
                          "upgraded": 1000,
                          "coexistence": 20,
                          "upgradedLevels": [
                            "HIGH",
                            "HIGH_THROUGHPUT"
                          ]
                        },
                        "refreshMax": 25
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`too_many_connections`: `refresh=true` over more than 25 numbers. Narrow with `companyId`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`out_of_scope`: a client-scoped key named another client in `companyId`. Also `account_deactivated`: the key belongs to a suspended client."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/numbers/{id}/health": {
      "get": {
        "operationId": "getNumberHealth",
        "tags": [
          "Numbers"
        ],
        "summary": "Read one number's live health from Meta",
        "description": "Reads one number's health live from Meta, stores it, and returns in `alerts` what changed since the previous stored read.\n\nWhen to use: before launching a campaign or a large batch, after a drop in delivery rate, or on a schedule to catch problems early. Quality falls to `YELLOW` before Meta restricts a number; a `messagingLimitTier` downgrade lowers how many unique customers can receive template messages in 24 hours; `sandbox: true` means the number only delivers to recipients registered on it. `alerts` is empty on the first read of a number (that read is the baseline) and when nothing moved; each alert has a `severity` (`info`, `warning`, `critical`) and a human-readable `message`.\n\n`status` and `metaStatus` are Meta's number status (`CONNECTED`, `FLAGGED`, `RESTRICTED`, …); `genukaStatus` is Genuka's own lifecycle value for the connection. Each call costs one Meta read; for a fleet overview use `GET /api/v1/numbers` instead.",
        "parameters": [
          {
            "$ref": "#/components/parameters/NumberConnectionIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Live health, the alerts it raised, and Meta's reference limits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NumberHealthResponse"
                },
                "examples": {
                  "healthy": {
                    "summary": "A healthy number, no change",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                        "wabaId": "102290129340398",
                        "phoneNumberId": "106540352242922",
                        "displayPhoneNumber": "+237 6 99 00 11 22",
                        "verifiedName": "Acme Coffee",
                        "qualityRating": "GREEN",
                        "messagingLimitTier": "TIER_1K",
                        "throughputLevel": "STANDARD",
                        "platformType": "CLOUD_API",
                        "status": "CONNECTED",
                        "codeVerificationStatus": "VERIFIED",
                        "accountMode": "LIVE",
                        "sandbox": false,
                        "coexistence": false,
                        "messagesPerSecond": 80,
                        "metaStatus": "CONNECTED",
                        "genukaStatus": "connected",
                        "offboardedAt": null
                      },
                      "alerts": [],
                      "limits": {
                        "throughput": {
                          "default": 80,
                          "upgraded": 1000,
                          "coexistence": 20,
                          "upgradedLevels": [
                            "HIGH",
                            "HIGH_THROUGHPUT"
                          ]
                        },
                        "tiers": [
                          "TIER_50",
                          "TIER_250",
                          "TIER_1K",
                          "TIER_10K",
                          "TIER_100K",
                          "TIER_UNLIMITED"
                        ]
                      }
                    }
                  },
                  "downgraded": {
                    "summary": "Tier downgrade detected",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                        "wabaId": "102290129340398",
                        "phoneNumberId": "106540352242922",
                        "qualityRating": "RED",
                        "messagingLimitTier": "TIER_250",
                        "throughputLevel": "STANDARD",
                        "platformType": "CLOUD_API",
                        "status": "FLAGGED",
                        "sandbox": false,
                        "coexistence": false,
                        "messagesPerSecond": 80,
                        "metaStatus": "FLAGGED",
                        "genukaStatus": "connected",
                        "offboardedAt": null
                      },
                      "alerts": [
                        {
                          "kind": "messaging_limit_changed",
                          "severity": "warning",
                          "from": "TIER_1K",
                          "to": "TIER_250",
                          "message": "Messaging limit was downgraded from TIER_1K to TIER_250. Any campaign sized for the old tier will now be throttled or rejected — re-plan before the next send."
                        }
                      ],
                      "limits": {
                        "throughput": {
                          "default": 80,
                          "upgraded": 1000,
                          "coexistence": 20,
                          "upgradedLevels": [
                            "HIGH",
                            "HIGH_THROUGHPUT"
                          ]
                        },
                        "tiers": [
                          "TIER_50",
                          "TIER_250",
                          "TIER_1K",
                          "TIER_10K",
                          "TIER_100K",
                          "TIER_UNLIMITED"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/connections": {
      "get": {
        "operationId": "listConnections",
        "tags": [
          "Connections"
        ],
        "summary": "List WhatsApp connections",
        "description": "Lists the WhatsApp connections the key can reach (one WhatsApp Business Account plus one phone number each), newest first, with the client that owns each one. A connection's `id` is the `connectionId` that sending messages, creating templates and campaigns, and every number-level endpoint expect.\n\nRight after onboarding, use `externalRef`: when you send a client to Embedded Signup with your own identifier (`/connect/<slug>?ref=<your-id>`), `GET /api/v1/connections?externalRef=<your-id>` returns exactly the number that client connected, with no guessing by recency. A ref that is not a valid identifier (1 to 128 characters from `A-Z a-z 0-9 . _ : -`) matches nothing and returns an empty list, not an error.\n\nFor health fields (tier, throughput, coexistence) use `GET /api/v1/numbers`. No pagination.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          },
          {
            "name": "externalRef",
            "in": "query",
            "required": false,
            "description": "Your own identifier for the client, as passed in `/connect/<slug>?ref=`. Exact match.",
            "schema": {
              "type": "string",
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9._:-]+$"
            },
            "example": "acme-7f3c2a"
          }
        ],
        "responses": {
          "200": {
            "description": "The connections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionListResponse"
                },
                "examples": {
                  "byRef": {
                    "summary": "?externalRef=acme-7f3c2a",
                    "value": {
                      "data": [
                        {
                          "id": "cm8x4k9r20003qz7hb2c6e8f4",
                          "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                          "wabaId": "102290129340398",
                          "wabaName": "Acme Coffee",
                          "phoneNumberId": "106540352242922",
                          "displayPhoneNumber": "+237 6 99 00 11 22",
                          "verifiedName": "Acme Coffee",
                          "qualityRating": "GREEN",
                          "status": "connected",
                          "isMain": true,
                          "companyName": "Acme Coffee",
                          "externalRef": "acme-7f3c2a"
                        }
                      ]
                    }
                  },
                  "noMatch": {
                    "summary": "Unknown or malformed ref",
                    "value": {
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`out_of_scope`: a client-scoped key named another client in `companyId`. Also `account_deactivated`: the key belongs to a suspended client."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook endpoints",
        "description": "Lists the webhook endpoints the key can manage, newest first, each with its delivery counters. The signing secret is never returned here. A client-scoped key sees only the endpoints that client created itself (with its own key or from the client portal), never the partner's endpoints that also cover its numbers. With `companyId`, only endpoints scoped to that client or to one of its numbers are returned (account-wide endpoints are left out).",
        "parameters": [
          {
            "$ref": "#/components/parameters/CompanyIdFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One endpoint",
                    "value": {
                      "data": [
                        {
                          "id": "cm8y1w3e50002ab4cx7z0q9r1",
                          "name": "example.com",
                          "url": "https://example.com/webhooks/whatsapp",
                          "events": [
                            "message.received",
                            "message.sent",
                            "message.delivered",
                            "message.read",
                            "message.failed",
                            "template.status_changed"
                          ],
                          "enabled": true,
                          "scope": "company",
                          "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                          "connectionId": null,
                          "createdAt": "2026-09-02T08:14:03.120Z",
                          "updatedAt": "2026-09-02T08:14:03.120Z",
                          "deliveries": {
                            "success": 1824,
                            "pending": 2,
                            "failed": 3
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`out_of_scope`: a client-scoped key named another client in `companyId`. Also `account_deactivated`: the key belongs to a suspended client."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook endpoint",
        "description": "Registers an HTTPS endpoint that receives Genuka WA events as signed `POST` requests: inbound customer messages, delivery statuses (`sent`, `delivered`, `read`, `failed`), template review outcomes, account and number changes. This is how an integration receives replies and delivery receipts: the REST API has no endpoint to poll for inbound messages.\n\nCoverage: `connectionId` → one number (takes precedence); `companyId` → every number of one client; neither, with a partner-wide key → every number on the account. A client-scoped key always creates a client- or number-level endpoint, and may register at most 3.\n\n`events` takes Genuka event names (see `WebhookEvent`) or legacy Meta field names; omitted, empty, or the full list all mean every event (returned as `events: []`). Unknown names are rejected rather than ignored.\n\nThe `201` response is the only time the signing `secret` (`whsec_…`) is returned: store it. Each delivery is a `POST` with `X-Genuka-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" keyed with the secret>`: verify it on the raw body and reject a `t` older than 300 seconds. Other headers: `X-Genuka-Event`, `X-Genuka-Event-Field`, `X-Genuka-Delivery` (stable across retries: de-duplicate on it), `X-Genuka-Webhook-Id`, `X-Genuka-Attempt`. Any 2xx within 10 seconds is a success; redirects are not followed (a 3xx counts as a failure). Otherwise the delivery is retried after 1 min, 5 min, 30 min, 2 h and 6 h, then marked `failed` and can be replayed.\n\nNext step: `POST /api/v1/webhooks/{id}/test` once your receiver is deployed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookCreateRequest"
              },
              "examples": {
                "client": {
                  "summary": "Every number of one client, messages and template reviews",
                  "value": {
                    "url": "https://example.com/webhooks/whatsapp",
                    "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                    "events": [
                      "message.received",
                      "message.delivered",
                      "message.read",
                      "message.failed",
                      "template.status_changed"
                    ]
                  }
                },
                "everything": {
                  "summary": "Every event, every number (partner-wide key)",
                  "value": {
                    "url": "https://hooks.example.com/genuka",
                    "name": "Production"
                  }
                },
                "oneNumber": {
                  "summary": "One number only",
                  "value": {
                    "url": "https://example.com/webhooks/store-2",
                    "connectionId": "cm8x5a1c70011qz7h3k9m2p6t"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Store `data.secret` now: it is never returned again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookWithSecretResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Created",
                    "value": {
                      "data": {
                        "id": "cm8y1w3e50002ab4cx7z0q9r1",
                        "name": "example.com",
                        "url": "https://example.com/webhooks/whatsapp",
                        "events": [
                          "message.received",
                          "message.sent",
                          "message.delivered",
                          "message.read",
                          "message.failed",
                          "template.status_changed"
                        ],
                        "enabled": true,
                        "scope": "company",
                        "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                        "connectionId": null,
                        "createdAt": "2026-09-02T08:14:03.120Z",
                        "updatedAt": "2026-09-02T08:14:03.120Z",
                        "deliveries": {
                          "success": 0,
                          "pending": 0,
                          "failed": 0
                        },
                        "secret": "whsec_3q2-xYcK8vN1mZ0pL7tR5wE9uI4oA6sD2fG8hJ1kL0"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`missing_fields` (no `url`, or a body that is not JSON), `invalid_url` (not https, credentials in the URL, private or unresolvable host; `message` says which), `invalid_events` (not an array of strings), `unknown_events` (`message` lists the unknown names)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`out_of_scope`: the client or number belongs to another client than the key's. `account_deactivated`: that client, or the key's own client, is suspended."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found` or `company_not_found`."
          },
          "409": {
            "description": "`webhook_limit_reached`: a client-scoped key may register at most 3 endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "limit": {
                    "summary": "Limit reached",
                    "value": {
                      "error": "webhook_limit_reached",
                      "message": "A client may register at most 3 endpoints"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookIdPath"
        }
      ],
      "get": {
        "operationId": "getWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Get a webhook endpoint",
        "description": "Returns one endpoint with its subscribed events and delivery counters. The signing secret is not included: it is shown only at creation and on rotation (`PATCH` with `rotateSecret: true`).",
        "responses": {
          "200": {
            "description": "The endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                },
                "examples": {
                  "one": {
                    "summary": "Endpoint",
                    "value": {
                      "data": {
                        "id": "cm8y1w3e50002ab4cx7z0q9r1",
                        "name": "example.com",
                        "url": "https://example.com/webhooks/whatsapp",
                        "events": [
                          "message.received",
                          "message.sent",
                          "message.delivered",
                          "message.read",
                          "message.failed",
                          "template.status_changed"
                        ],
                        "enabled": true,
                        "scope": "company",
                        "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                        "connectionId": null,
                        "createdAt": "2026-09-02T08:14:03.120Z",
                        "updatedAt": "2026-09-02T08:14:03.120Z",
                        "deliveries": {
                          "success": 1824,
                          "pending": 2,
                          "failed": 3
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`webhook_not_found`: no endpoint with this id within the key's scope."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook endpoint or rotate its secret",
        "description": "Partially updates an endpoint: only the fields present change.\n\n- `events: []` resets the subscription to every event.\n- `enabled: false` pauses the endpoint: new events are no longer queued for it, and a retry that comes due while it is disabled is settled as `failed` (`endpoint_disabled`). Re-enable before replaying.\n- `rotateSecret: true` generates a new signing secret, returned in this response only. The previous secret stops verifying immediately, so deploy the new one in the same window.\n\nCoverage (`companyId`, `connectionId`) cannot be changed: delete the endpoint and create a new one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookUpdateRequest"
              },
              "examples": {
                "pause": {
                  "summary": "Pause deliveries",
                  "value": {
                    "enabled": false
                  }
                },
                "events": {
                  "summary": "Change the subscription",
                  "value": {
                    "events": [
                      "message.received",
                      "user_preference.stopped"
                    ]
                  }
                },
                "rotate": {
                  "summary": "Rotate the signing secret",
                  "value": {
                    "rotateSecret": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated endpoint; `data.secret` only after a rotation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookUpdateResponse"
                },
                "examples": {
                  "rotated": {
                    "summary": "After rotateSecret: true",
                    "value": {
                      "data": {
                        "id": "cm8y1w3e50002ab4cx7z0q9r1",
                        "name": "example.com",
                        "url": "https://example.com/webhooks/whatsapp",
                        "events": [
                          "message.received",
                          "message.sent",
                          "message.delivered",
                          "message.read",
                          "message.failed",
                          "template.status_changed"
                        ],
                        "enabled": true,
                        "scope": "company",
                        "companyId": "cm8x4k2p10000qz7h5n3d9w1a",
                        "connectionId": null,
                        "createdAt": "2026-09-02T08:14:03.120Z",
                        "updatedAt": "2026-10-08T09:00:00.000Z",
                        "deliveries": {
                          "success": 1824,
                          "pending": 2,
                          "failed": 3
                        },
                        "secret": "whsec_Yk3mN0pQ7rS1tU4vW8xZ2aB5cD9eF6gH0iJ3kL7mN1"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`invalid_json` (body missing or not JSON), `invalid_url`, `invalid_events`, `unknown_events`, `invalid_name` (blank name)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`webhook_not_found`."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook endpoint",
        "description": "Deletes the endpoint and its delivery log. Irreversible: nothing more is delivered to it, and its pending retries are dropped. To stop deliveries temporarily, `PATCH` it with `enabled: false` instead.",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeleteResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Deleted",
                    "value": {
                      "ok": true,
                      "deleted": "cm8y1w3e50002ab4cx7z0q9r1"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`webhook_not_found`."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookIdPath"
        }
      ],
      "post": {
        "operationId": "testWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send signed test events to an endpoint",
        "description": "Sends 8 synthetic events, one per event family (`inbound_message`, `message_status`, `template_status`, `template.quality_changed`, `account_update`, `phone_quality`, `user_preference.stopped`, `unknown.received`), to the endpoint now, and reports how it answered each one.\n\nEach request is signed exactly like a real delivery, with the endpoint's current secret and the same envelope, so a receiver that passes has really implemented signature verification, not merely answered 200. Test requests carry `X-Genuka-Test: true` (skip side effects on it), an `id` starting with `test_`, and null `waba_id` / `phone_number_id`. Nothing is written to the delivery log and nothing is retried. Each request times out after 5 seconds; at most 4 run at once. The test runs even when the endpoint is disabled. No request body.",
        "responses": {
          "200": {
            "description": "Per-event results. A 200 here only means the test ran: read `failed` and `results`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                },
                "examples": {
                  "partial": {
                    "summary": "Receiver rejects one event",
                    "value": {
                      "data": {
                        "url": "https://example.com/webhooks/whatsapp",
                        "sent": 8,
                        "succeeded": 7,
                        "failed": 1,
                        "results": [
                          {
                            "event": "inbound_message",
                            "field": "messages",
                            "status": 200,
                            "latencyMs": 182,
                            "ok": true,
                            "error": null
                          },
                          {
                            "event": "message_status",
                            "field": "messages",
                            "status": 200,
                            "latencyMs": 167,
                            "ok": true,
                            "error": null
                          },
                          {
                            "event": "unknown.received",
                            "field": "some_future_field",
                            "status": 400,
                            "latencyMs": 95,
                            "ok": false,
                            "error": "http_400"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`webhook_not_found`."
          },
          "409": {
            "description": "`no_secret`: an endpoint created before signing existed. Rotate its secret (`PATCH` with `rotateSecret: true`) first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "noSecret": {
                    "summary": "No signing secret",
                    "value": {
                      "error": "no_secret",
                      "message": "Rotate this endpoint's secret before testing it"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/deliveries": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookIdPath"
        }
      ],
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": [
          "Webhooks"
        ],
        "summary": "List an endpoint's deliveries",
        "description": "Returns the delivery log of one endpoint, newest first: every event queued for it, with its status, attempts, your server's last HTTP status and error, and the exact payload.\n\nWhen to use: to debug a receiver (what did it answer, and to which event?) and to find failed deliveries to replay with `POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay`. `status=failed` lists what was given up on. Paginate by passing `meta.nextCursor` as `cursor` until it is null. Deliveries older than the plan's retention (`meta.retentionDays`) are purged.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter on delivery status.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "success",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1 to 200 (values above 200 are clamped).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "`meta.nextCursor` from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryListResponse"
                },
                "examples": {
                  "page": {
                    "summary": "Failed deliveries",
                    "value": {
                      "data": [
                        {
                          "id": "cm8y2d7f90015ab4c5u3v8n2k",
                          "event": "message_status",
                          "field": "messages",
                          "eventKey": "wamid.HBgLMjM3Njk5MDAxMTIyFQIAERgSQzVGOEI4:delivered",
                          "status": "failed",
                          "attempts": 6,
                          "maxAttempts": 6,
                          "responseStatus": 503,
                          "error": "http_503",
                          "nextAttemptAt": null,
                          "deliveredAt": null,
                          "createdAt": "2026-10-07T14:02:11.420Z",
                          "updatedAt": "2026-10-08T00:32:45.010Z",
                          "payload": {
                            "type": "message_status",
                            "field": "messages",
                            "created_at": "2026-10-07T14:02:11.408Z",
                            "partner_id": "cm8w0p2t10000xy9z8a7b6c5d",
                            "company_id": "cm8x4k2p10000qz7h5n3d9w1a",
                            "connection_id": "cm8x4k9r20003qz7hb2c6e8f4",
                            "waba_id": "102290129340398",
                            "phone_number_id": "106540352242922",
                            "data": {
                              "id": "wamid.HBgLMjM3Njk5MDAxMTIyFQIAERgSQzVGOEI4",
                              "status": "delivered",
                              "timestamp": "1791381731",
                              "recipient_id": "237699001122"
                            }
                          }
                        }
                      ],
                      "meta": {
                        "nextCursor": "cm8y2c4b10014ab4c9t2w6m1j",
                        "retentionDays": 30
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`invalid_status` (not pending, success or failed) or `invalid_limit` (not a number ≥ 1)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`webhook_not_found`."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay": {
      "parameters": [
        {
          "$ref": "#/components/parameters/WebhookIdPath"
        },
        {
          "$ref": "#/components/parameters/WebhookDeliveryIdPath"
        }
      ],
      "post": {
        "operationId": "replayWebhookDelivery",
        "tags": [
          "Webhooks"
        ],
        "summary": "Replay a delivery",
        "description": "Re-sends one delivery, typically after fixing a receiver outage. The attempt counter is reset (a full retry budget again), the delivery goes back to `pending`, and it is attempted right after this `202` response. The body is identical to the original (same `id`, same payload) with a fresh signature and timestamp, so idempotent receivers keyed on `id` or `X-Genuka-Delivery` handle it safely.\n\nWorks on `success` and `failed` deliveries. Refused while the delivery is still `pending` (it is already queued) and while the endpoint is disabled. No request body. Follow the outcome with `GET /api/v1/webhooks/{id}/deliveries`.",
        "responses": {
          "202": {
            "description": "Queued; attempted right after the response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookReplayResponse"
                },
                "examples": {
                  "queued": {
                    "summary": "Queued",
                    "value": {
                      "data": {
                        "id": "cm8y2d7f90015ab4c5u3v8n2k",
                        "status": "pending",
                        "queued": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`delivery_not_found`: no such delivery on this endpoint within the key's scope."
          },
          "409": {
            "description": "`delivery_pending`: already queued for retry. `webhook_disabled`: enable the endpoint (`PATCH` with `enabled: true`) before replaying.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "pending": {
                    "summary": "Already queued",
                    "value": {
                      "error": "delivery_pending",
                      "message": "This delivery is already queued for retry"
                    }
                  },
                  "disabled": {
                    "summary": "Endpoint disabled",
                    "value": {
                      "error": "webhook_disabled",
                      "message": "Enable this endpoint before replaying to it"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/companies": {
      "get": {
        "operationId": "listCompanies",
        "tags": [
          "Companies"
        ],
        "summary": "List clients (connected businesses)",
        "description": "Lists the clients the key can reach (the businesses whose WhatsApp numbers are connected to the account), most recently onboarded first, each with its connections and template count. A client-scoped key gets exactly one row: its own client. `externalRef` echoes the identifier you passed in the Embedded Signup link (`/connect/<slug>?ref=`), to reconcile clients with your own records. Suspended clients are listed too: `GET /api/v1/companies/{id}` tells them apart. No pagination.",
        "responses": {
          "200": {
            "description": "The clients.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One client",
                    "value": {
                      "data": [
                        {
                          "id": "cm8x4k2p10000qz7h5n3d9w1a",
                          "name": "Acme Coffee",
                          "externalRef": "acme-7f3c2a",
                          "onboardedAt": "2026-06-01T10:12:00.000Z",
                          "connections": [
                            {
                              "id": "cm8x4k9r20003qz7hb2c6e8f4",
                              "wabaId": "102290129340398",
                              "phoneNumberId": "106540352242922",
                              "displayPhoneNumber": "+237 6 99 00 11 22",
                              "qualityRating": "GREEN",
                              "status": "connected"
                            }
                          ],
                          "_count": {
                            "templates": 4
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the key belongs to a suspended client."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/companies/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyIdPath"
        }
      ],
      "get": {
        "operationId": "getCompany",
        "tags": [
          "Companies"
        ],
        "summary": "Get a client",
        "description": "Returns one client with its status, currency, connections (with `verifiedName` and `isMain`) and its template and campaign counts. A suspended client answers `403 account_deactivated`: while suspended, its numbers cannot be used through any key.",
        "responses": {
          "200": {
            "description": "The client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyResponse"
                },
                "examples": {
                  "one": {
                    "summary": "Client",
                    "value": {
                      "data": {
                        "id": "cm8x4k2p10000qz7h5n3d9w1a",
                        "partnerId": "cm8w0p2t10000xy9z8a7b6c5d",
                        "name": "Acme Coffee",
                        "status": "active",
                        "deactivatedAt": null,
                        "currency": "XAF",
                        "externalRef": "acme-7f3c2a",
                        "apiAccessEnabled": false,
                        "onboardedAt": "2026-06-01T10:12:00.000Z",
                        "connections": [
                          {
                            "id": "cm8x4k9r20003qz7hb2c6e8f4",
                            "wabaId": "102290129340398",
                            "phoneNumberId": "106540352242922",
                            "displayPhoneNumber": "+237 6 99 00 11 22",
                            "verifiedName": "Acme Coffee",
                            "qualityRating": "GREEN",
                            "status": "connected",
                            "isMain": true
                          }
                        ],
                        "_count": {
                          "templates": 4,
                          "campaigns": 2
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: this client, or the key's own client, is suspended."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`company_not_found`: no client with this id within the key's scope."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/profile": {
      "get": {
        "operationId": "getBusinessProfile",
        "tags": [
          "Business profile"
        ],
        "summary": "Read a number's WhatsApp Business profile",
        "description": "Reads, live from Meta, the WhatsApp Business profile of a number: the card customers see when they tap the business name (about line, address, description, email, websites, business category, profile picture URL). Every field is optional; a fresh number has an empty profile.\n\nWhich number: `connectionId` (the Genuka connection `id` from `GET /api/v1/connections` or `GET /api/v1/numbers`). It is required with a partner-wide key (`400 connection_required` otherwise); with a client-scoped key it may be omitted and defaults to the client's main number (its oldest number if none is marked main).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileResponse"
                },
                "examples": {
                  "profile": {
                    "summary": "Profile",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "phoneNumberId": "106540352242922",
                        "profile": {
                          "about": "Specialty coffee, roasted in Douala",
                          "address": "Rue Joss, Bonanjo, Douala",
                          "description": "Order beans and drinks for delivery or pick-up.",
                          "email": "hello@example.com",
                          "websites": [
                            "https://example.com"
                          ],
                          "vertical": "RESTAURANT",
                          "profilePictureUrl": "https://pps.whatsapp.net/v/t61.24694-24/123456789_n.jpg"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`: a partner-wide key must pass `connectionId`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateBusinessProfile",
        "tags": [
          "Business profile"
        ],
        "summary": "Update a number's WhatsApp Business profile",
        "description": "Updates the WhatsApp Business profile of a number. Partial: only the fields present in the body are sent to Meta; the others are left untouched. The response is the profile re-read from Meta after the write, since Meta may normalize what it stores.\n\nLimits are checked before calling Meta, because Meta silently drops what it does not accept: `about` ≤ 139 characters, `address` ≤ 256, `description` ≤ 512, `email` ≤ 128 and shaped like an address, at most 2 `websites` (absolute http or https URLs including the scheme, ≤ 256 characters each), `vertical` from the fixed list. `profilePictureHandle` is a handle from Meta's Resumable Upload API, not a media id or URL. A body with no profile field is refused.\n\nWhich number: `connectionId` in the body; required with a partner-wide key, defaults to the client's main number with a client-scoped key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfileUpdateRequest"
              },
              "examples": {
                "about": {
                  "summary": "Change the about line and websites",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "about": "Specialty coffee, roasted in Douala",
                    "websites": [
                      "https://example.com",
                      "https://example.com/menu"
                    ]
                  }
                },
                "vertical": {
                  "summary": "Set the business category",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "vertical": "RESTAURANT"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The profile as Meta now holds it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileResponse"
                },
                "examples": {
                  "profile": {
                    "summary": "Profile",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "phoneNumberId": "106540352242922",
                        "profile": {
                          "about": "Specialty coffee, roasted in Douala",
                          "address": "Rue Joss, Bonanjo, Douala",
                          "description": "Order beans and drinks for delivery or pick-up.",
                          "email": "hello@example.com",
                          "websites": [
                            "https://example.com"
                          ],
                          "vertical": "RESTAURANT",
                          "profilePictureUrl": "https://pps.whatsapp.net/v/t61.24694-24/123456789_n.jpg"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required` (partner-wide key without `connectionId`), or `invalid_profile` with the field and rule in `message`, e.g. `profile.about: must be at most 139 characters (got 150)`, `profile.websites[0]: must be an absolute URL including the scheme …`, or `profile: no field to update — send at least one profile field`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/subscription": {
      "get": {
        "operationId": "getSubscription",
        "tags": [
          "Billing"
        ],
        "summary": "Get the subscription, limits and usage",
        "description": "Returns the account's subscription: plan, billing interval and currency, status, end of the current period, the number of WhatsApp numbers paid for (`billedNumbers`), usage so far and the effective limits for this period. Partner-wide keys only.\n\nWhen to use: before a campaign or a bulk send, compare `usage.messages` with `limits.monthlyMessages`. A send that would exceed the allowance is refused with `402 plan_limit_messages` (a campaign is checked against its whole recipient list before the first send); connecting a number beyond `limits.maxNumbers` is refused with `402 plan_limit_numbers`; while `status` is `past_due` (trial or paid period lapsed) sends are refused with `402 subscription_past_due`. A `null` limit means unlimited.\n\nThe allowance counts messages Meta accepted through Genuka WA. It is a subscription quota, not a charge: Meta bills its message rates directly to the client's WhatsApp Business Account, with no markup from Genuka.",
        "responses": {
          "200": {
            "description": "The subscription. Not wrapped in `data`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionResponse"
                },
                "examples": {
                  "active": {
                    "summary": "Active subscription",
                    "value": {
                      "plan": {
                        "code": "growth",
                        "name": "Growth"
                      },
                      "interval": "monthly",
                      "currency": "XAF",
                      "status": "active",
                      "currentPeriodEnd": "2026-10-18T00:00:00.000Z",
                      "billedNumbers": 3,
                      "usage": {
                        "numbers": 3,
                        "messages": 4180,
                        "seats": 2
                      },
                      "limits": {
                        "maxNumbers": 3,
                        "monthlyMessages": 15000,
                        "maxSeats": 5
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`partner_key_required`: billing is readable with a partner-wide key only. Also `account_deactivated`: the key belongs to a suspended client."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/invoices": {
      "get": {
        "operationId": "listInvoices",
        "tags": [
          "Billing"
        ],
        "summary": "List invoices",
        "description": "Lists the account's subscription invoices, newest first (at most 100, no pagination). Partner-wide keys only. Amounts are integers in minor units of `currency`: XAF and XOF have no minor unit (15000 = 15,000 XAF), EUR and USD are in cents. `status` is stored (`open`, `paid`, `void`); `state` adds the clock (`due`, or `overdue` once an open invoice is past `dueAt`). Filter with `status`; any other value is ignored and every invoice is returned. Read-only: invoices are settled from the dashboard. They bill the Genuka subscription only; Meta's message charges never appear here.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filter on stored status.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "paid",
                "void"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The invoices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One open invoice",
                    "value": {
                      "data": [
                        {
                          "id": "cm8z0i4n30001de6f2g8h5j7l",
                          "number": "INV-2026-0042",
                          "status": "open",
                          "kind": "subscription",
                          "state": "due",
                          "plan": {
                            "code": "growth",
                            "name": "Growth"
                          },
                          "interval": "monthly",
                          "currency": "XAF",
                          "numbers": 3,
                          "amountDue": 14400,
                          "referralCreditAmount": 0,
                          "welcomeDiscountAmount": 0,
                          "periodStart": "2026-10-18T00:00:00.000Z",
                          "periodEnd": "2026-11-18T00:00:00.000Z",
                          "issuedAt": "2026-10-11T00:00:00.000Z",
                          "dueAt": "2026-10-18T00:00:00.000Z",
                          "paidAt": null
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`partner_key_required`. Also `account_deactivated`: the key belongs to a suspended client."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/invoices/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/InvoiceIdPath"
        }
      ],
      "get": {
        "operationId": "getInvoice",
        "tags": [
          "Billing"
        ],
        "summary": "Get an invoice with its payment attempts",
        "description": "Returns one invoice and the payment attempts made against it (newest first), each with its provider (`genuka_pay`, `pawapay`, `stripe`, `referral_credit`, `manual`) and status. Partner-wide keys only. The response is not wrapped in `data`.",
        "responses": {
          "200": {
            "description": "The invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceDetail"
                },
                "examples": {
                  "paid": {
                    "summary": "Paid invoice",
                    "value": {
                      "id": "cm8z0i4n30001de6f2g8h5j7l",
                      "number": "INV-2026-0042",
                      "status": "paid",
                      "kind": "subscription",
                      "state": "paid",
                      "plan": {
                        "code": "growth",
                        "name": "Growth"
                      },
                      "interval": "monthly",
                      "currency": "XAF",
                      "numbers": 3,
                      "amountDue": 14400,
                      "referralCreditAmount": 0,
                      "welcomeDiscountAmount": 0,
                      "periodStart": "2026-10-18T00:00:00.000Z",
                      "periodEnd": "2026-11-18T00:00:00.000Z",
                      "issuedAt": "2026-10-11T00:00:00.000Z",
                      "dueAt": "2026-10-18T00:00:00.000Z",
                      "paidAt": "2026-10-16T09:41:00.000Z",
                      "payments": [
                        {
                          "id": "cm8z0p6q80004de6fk1m3n9s2",
                          "provider": "genuka_pay",
                          "status": "success",
                          "amount": 14400,
                          "currency": "XAF",
                          "createdAt": "2026-10-16T09:40:12.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`partner_key_required`. Also `account_deactivated`: the key belongs to a suspended client."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`not_found` (\"Invoice not found.\")."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/analytics": {
      "get": {
        "operationId": "getAnalytics",
        "tags": [
          "Analytics"
        ],
        "summary": "Read messaging, conversation or cost analytics",
        "description": "Returns Meta's analytics for the WhatsApp Business Account behind a number, flattened into `points` and rolled up per UTC day in `days`. Every number of that account is included unless `phoneNumbers` narrows it.\n\n`kind`: `messaging` (default) = messages sent and delivered; `conversation` = conversation counts and cost; `pricing` = message volume and cost. Window: `start`/`end` as unix seconds or any date string (`2026-09-01`); defaults to the last 30 days.\n\nCost is never defaulted to zero: when Meta reports none (always for `messaging`; for accounts billed through a partner credit line) each `cost` is `{ available: false, reason }` and `costAvailable` is false. Do not add unavailable costs up as zero.\n\nMeta only serves the last 365 days: an older `start` is moved forward, `truncated` is set and `notice` explains. Every read is archived by Genuka per client and UTC day; `source=archive` reads that archive instead of Meta (no Meta call, and the only way to see data older than a year), returning the stored days only.\n\nWhich number: `connectionId` (the Genuka connection `id` from `GET /api/v1/connections` or `GET /api/v1/numbers`). It is required with a partner-wide key (`400 connection_required` otherwise); with a client-scoped key it may be omitted and defaults to the client's main number (its oldest number if none is marked main).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/AnalyticsKind"
            },
            "description": "Defaults to `messaging`."
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "Start of the window: unix seconds or a date string. Defaults to 30 days before `end`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01"
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "End of the window: unix seconds or a date string. Defaults to now. Must be after `start`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-01"
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "`archive` reads Genuka's archive instead of Meta. The filters below do not apply to the archive.",
            "schema": {
              "type": "string",
              "enum": [
                "archive"
              ]
            }
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "description": "`messaging`: `HALF_HOUR`, `DAY` (default), `MONTH`. `conversation` and `pricing`: `HALF_HOUR`, `DAILY` (default), `MONTHLY`.",
            "schema": {
              "type": "string",
              "enum": [
                "HALF_HOUR",
                "DAY",
                "MONTH",
                "DAILY",
                "MONTHLY"
              ]
            }
          },
          {
            "name": "phoneNumbers",
            "in": "query",
            "required": false,
            "description": "Comma-separated phone numbers (digits with country code, e.g. `237699001122`) to restrict to.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "countryCodes",
            "in": "query",
            "required": false,
            "description": "`messaging` only: comma-separated recipient country codes (e.g. `CM,CI`). Ignored for other kinds.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dimensions",
            "in": "query",
            "required": false,
            "description": "`conversation` and `pricing` only: comma-separated Meta dimensions to break points down by (conversation: `CONVERSATION_CATEGORY`, `DIRECTION`, `TYPE`, `COUNTRY`, `PHONE`; pricing: `COUNTRY`, `PHONE`, `PRICING_CATEGORY`, `PRICING_TYPE`, `TIER`). Passed to Meta as-is.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "metricTypes",
            "in": "query",
            "required": false,
            "description": "`pricing` only: comma-separated `COST`, `VOLUME`. Passed to Meta as-is.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics from Meta (default) or from the archive (`source=archive`).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/AnalyticsResponse"
                    },
                    {
                      "$ref": "#/components/schemas/AnalyticsArchiveResponse"
                    }
                  ],
                  "description": "`data.source` tells the two shapes apart: `meta` (default) or `archive` (`source=archive`)."
                },
                "examples": {
                  "messaging": {
                    "summary": "kind=messaging",
                    "value": {
                      "data": {
                        "kind": "messaging",
                        "source": "meta",
                        "granularity": "DAY",
                        "points": [
                          {
                            "start": 1790985600,
                            "end": 1791072000,
                            "day": "2026-10-03",
                            "sent": 412,
                            "delivered": 398,
                            "cost": {
                              "available": false,
                              "reason": "Messaging analytics never carries cost. Ask for kind=pricing or kind=conversation instead."
                            },
                            "dimensions": {}
                          }
                        ],
                        "days": [
                          {
                            "day": "2026-10-03",
                            "totals": {
                              "sent": 412,
                              "delivered": 398,
                              "cost": {
                                "available": false,
                                "reason": "Messaging analytics never carries cost. Ask for kind=pricing or kind=conversation instead."
                              }
                            },
                            "points": [
                              {
                                "start": 1790985600,
                                "end": 1791072000,
                                "day": "2026-10-03",
                                "sent": 412,
                                "delivered": 398,
                                "cost": {
                                  "available": false,
                                  "reason": "Messaging analytics never carries cost. Ask for kind=pricing or kind=conversation instead."
                                },
                                "dimensions": {}
                              }
                            ]
                          }
                        ]
                      },
                      "window": {
                        "start": "2026-10-03T00:00:00.000Z",
                        "end": "2026-10-04T00:00:00.000Z"
                      },
                      "truncated": false,
                      "archivedDays": 1,
                      "costAvailable": false,
                      "costNotice": "Messaging analytics never carries cost. Ask for kind=pricing or kind=conversation instead.",
                      "retentionDays": 365
                    }
                  },
                  "pricing": {
                    "summary": "kind=pricing&dimensions=PRICING_CATEGORY",
                    "value": {
                      "data": {
                        "kind": "pricing",
                        "source": "meta",
                        "granularity": "DAILY",
                        "points": [
                          {
                            "start": 1790985600,
                            "end": 1791072000,
                            "day": "2026-10-03",
                            "volume": 380,
                            "cost": {
                              "available": true,
                              "amount": 4.56
                            },
                            "dimensions": {
                              "pricing_category": "MARKETING"
                            }
                          }
                        ],
                        "days": [
                          {
                            "day": "2026-10-03",
                            "totals": {
                              "volume": 380,
                              "cost": {
                                "available": true,
                                "amount": 4.56
                              }
                            },
                            "points": [
                              {
                                "start": 1790985600,
                                "end": 1791072000,
                                "day": "2026-10-03",
                                "volume": 380,
                                "cost": {
                                  "available": true,
                                  "amount": 4.56
                                },
                                "dimensions": {
                                  "pricing_category": "MARKETING"
                                }
                              }
                            ]
                          }
                        ]
                      },
                      "window": {
                        "start": "2026-10-03T00:00:00.000Z",
                        "end": "2026-10-04T00:00:00.000Z"
                      },
                      "truncated": false,
                      "archivedDays": 1,
                      "costAvailable": true,
                      "retentionDays": 365
                    }
                  },
                  "noCost": {
                    "summary": "kind=conversation on a WABA with no reported cost",
                    "value": {
                      "data": {
                        "kind": "conversation",
                        "source": "meta",
                        "granularity": "DAILY",
                        "points": [],
                        "days": []
                      },
                      "window": {
                        "start": "2026-09-08T00:00:00.000Z",
                        "end": "2026-10-08T00:00:00.000Z"
                      },
                      "truncated": false,
                      "archivedDays": 0,
                      "costAvailable": false,
                      "costNotice": "Meta did not report a cost for this period. WABAs billed through a partner credit line are excluded from cost analytics — this is not a zero, and it must not be added up as one.",
                      "retentionDays": 365
                    }
                  },
                  "archive": {
                    "summary": "source=archive",
                    "value": {
                      "data": {
                        "kind": "messaging",
                        "source": "archive",
                        "days": [
                          {
                            "day": "2025-03-14",
                            "kind": "messaging",
                            "metrics": {
                              "sourceGranularity": "DAY",
                              "totals": {
                                "sent": 120,
                                "delivered": 117,
                                "cost": {
                                  "available": false,
                                  "reason": "Messaging analytics never carries cost. Ask for kind=pricing or kind=conversation instead."
                                }
                              },
                              "points": []
                            },
                            "updatedAt": "2025-03-15T02:00:04.000Z"
                          }
                        ]
                      },
                      "window": {
                        "start": "2025-03-01T00:00:00.000Z",
                        "end": "2025-03-31T00:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required` (partner-wide key without `connectionId`), `invalid_kind`, `invalid_date` (`start`/`end` not a date or unix timestamp), or `invalid_analytics_query` (unsupported granularity for the kind, or `end` not after `start`)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/blocked-users": {
      "get": {
        "operationId": "listBlockedUsers",
        "tags": [
          "Blocked users"
        ],
        "summary": "List blocked users of a number",
        "description": "Lists the WhatsApp users blocked on a number, read live from Meta. Paginate by passing `paging.after` as `after`; `paging` is absent on the last page.\n\nWhich number: `connectionId` (the Genuka connection `id` from `GET /api/v1/connections` or `GET /api/v1/numbers`). It is required with a partner-wide key (`400 connection_required` otherwise); with a client-scoped key it may be omitted and defaults to the client's main number (its oldest number if none is marked main).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size passed to Meta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 100
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "`paging.after` from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of blocked users.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedUsersListResponse"
                },
                "examples": {
                  "page": {
                    "summary": "Page with more",
                    "value": {
                      "data": [
                        {
                          "waId": "237699001122"
                        },
                        {
                          "waId": "2250701020304"
                        }
                      ],
                      "paging": {
                        "after": "QVFIUmxtd3BmS2Y3Q0FB"
                      },
                      "connectionId": "cm8x4k9r20003qz7hb2c6e8f4"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`: a partner-wide key must pass `connectionId`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "blockUsers",
        "tags": [
          "Blocked users"
        ],
        "summary": "Block users on a number",
        "description": "Blocks WhatsApp users on a number so they can no longer message it. Meta only allows blocking a user who has messaged the number within the last 24 hours; others come back as failed with code `131047`, which cannot be checked before the call.\n\nOutcome is itemised, never a blanket answer: `200` when every number went through, `207` when some did and some did not, `422` (`error: \"block_failed\"`) when none did. Each failure carries Meta's own `message`, its `code` and, when known, a `hint`. A number Meta did not mention at all is reported as failed, not assumed done.\n\nNumbers are normalized to digits (spaces, dashes, dots, parentheses and a leading `+` removed) and de-duplicated; at most 1,000 per request, 64,000 on a number's blocklist in total. Send them as a `numbers` array in the body, or as `?numbers=` comma-separated.\n\nWhich number: `connectionId` in the body (required with a partner-wide key; defaults to the client's main number with a client-scoped key).",
        "parameters": [
          {
            "name": "numbers",
            "in": "query",
            "required": false,
            "description": "Comma-separated numbers, used only when the body carries none. (`connectionId` is read from the body only.)",
            "schema": {
              "type": "string"
            },
            "example": "237699001122,2250701020304"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockedUsersMutationRequest"
              },
              "examples": {
                "block": {
                  "summary": "Block two numbers",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "numbers": [
                      "+237 699 00 11 22",
                      "+2250701020304"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every number was blocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedUsersOutcomeResponse"
                },
                "examples": {
                  "all": {
                    "summary": "All blocked",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "action": "block",
                        "requested": 1,
                        "succeeded": [
                          {
                            "number": "237699001122",
                            "waId": "237699001122"
                          }
                        ],
                        "failed": []
                      },
                      "limits": {
                        "perRequestMax": 1000,
                        "totalMax": 64000
                      }
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "Some numbers were blocked, others not: read `data.failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedUsersOutcomeResponse"
                },
                "examples": {
                  "partial": {
                    "summary": "Partial",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "action": "block",
                        "requested": 2,
                        "succeeded": [
                          {
                            "number": "237699001122",
                            "waId": "237699001122"
                          }
                        ],
                        "failed": [
                          {
                            "number": "2250701020304",
                            "code": 131047,
                            "message": "Re-engagement message",
                            "hint": "this number has not written to you in the last 24 hours — Meta only allows blocking someone whose conversation is still open"
                          }
                        ]
                      },
                      "limits": {
                        "perRequestMax": 1000,
                        "totalMax": 64000
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`, `numbers_required` (no numbers in body or query), or `invalid_request` (`message` names the field: empty list, more than 1,000 numbers, or a value that is not a phone number)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Either no number went through (`error: \"block_failed\"`, same itemised body as 200/207), or Meta refused the whole call (`meta_error`, Error body with `meta`).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/BlockedUsersOutcomeResponse"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "noneBlocked": {
                    "summary": "Nothing blocked",
                    "value": {
                      "error": "block_failed",
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "action": "block",
                        "requested": 1,
                        "succeeded": [],
                        "failed": [
                          {
                            "number": "2250701020304",
                            "code": 131047,
                            "message": "Re-engagement message",
                            "hint": "this number has not written to you in the last 24 hours — Meta only allows blocking someone whose conversation is still open"
                          }
                        ]
                      },
                      "limits": {
                        "perRequestMax": 1000,
                        "totalMax": 64000
                      }
                    }
                  },
                  "metaRefused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "unblockUsers",
        "tags": [
          "Blocked users"
        ],
        "summary": "Unblock users on a number",
        "description": "Removes WhatsApp users from a number's blocklist.\n\nOutcome is itemised, never a blanket answer: `200` when every number went through, `207` when some did and some did not, `422` (`error: \"block_failed\"`) when none did. Each failure carries Meta's own `message`, its `code` and, when known, a `hint`. A number Meta did not mention at all is reported as failed, not assumed done.\n\nNumbers are normalized to digits (spaces, dashes, dots, parentheses and a leading `+` removed) and de-duplicated; at most 1,000 per request, 64,000 on a number's blocklist in total. Send them as a `numbers` array in the body, or as `?numbers=` comma-separated.\n\nSome HTTP clients drop the body of a DELETE: `connectionId` and `numbers` may also be passed in the query string. Which number: required with a partner-wide key; defaults to the client's main number with a client-scoped key.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          },
          {
            "name": "numbers",
            "in": "query",
            "required": false,
            "description": "Comma-separated numbers, used when the body carries none.",
            "schema": {
              "type": "string"
            },
            "example": "237699001122,2250701020304"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlockedUsersMutationRequest"
              },
              "examples": {
                "unblock": {
                  "summary": "Unblock one number",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "numbers": [
                      "+237699001122"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every number was unblocked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedUsersOutcomeResponse"
                },
                "examples": {
                  "all": {
                    "summary": "All unblocked",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "action": "unblock",
                        "requested": 1,
                        "succeeded": [
                          {
                            "number": "237699001122",
                            "waId": "237699001122"
                          }
                        ],
                        "failed": []
                      },
                      "limits": {
                        "perRequestMax": 1000,
                        "totalMax": 64000
                      }
                    }
                  }
                }
              }
            }
          },
          "207": {
            "description": "Some numbers were unblocked, others not: read `data.failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockedUsersOutcomeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`, `numbers_required`, or `invalid_request`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "No number went through (`block_failed`, itemised), or Meta refused the whole call (`meta_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/BlockedUsersOutcomeResponse"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/conversation-settings": {
      "get": {
        "operationId": "getConversationSettings",
        "tags": [
          "Conversation settings"
        ],
        "summary": "Read welcome message, ice breakers and commands",
        "description": "Reads, live from Meta, a number's conversational automation: whether the welcome trigger is enabled, the ice breakers (tappable suggestions shown in a chat with no history) and the slash commands offered when a customer types `/`. `limits` gives Meta's caps for building a form.\n\nWhich number: `connectionId` (the Genuka connection `id` from `GET /api/v1/connections` or `GET /api/v1/numbers`). It is required with a partner-wide key (`400 connection_required` otherwise); with a client-scoped key it may be omitted and defaults to the client's main number (its oldest number if none is marked main).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationSettingsResponse"
                },
                "examples": {
                  "settings": {
                    "summary": "Settings",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "phoneNumberId": "106540352242922",
                        "welcomeMessageEnabled": true,
                        "iceBreakers": [
                          "See the menu",
                          "Track my order",
                          "Opening hours"
                        ],
                        "commands": [
                          {
                            "name": "menu",
                            "description": "Today's menu and prices"
                          },
                          {
                            "name": "order",
                            "description": "Status of your last order"
                          }
                        ]
                      },
                      "limits": {
                        "iceBreakersMax": 4,
                        "iceBreakerMax": 80,
                        "commandsMax": 30,
                        "commandNameMax": 32,
                        "commandDescriptionMax": 256
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`: a partner-wide key must pass `connectionId`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateConversationSettings",
        "tags": [
          "Conversation settings"
        ],
        "summary": "Update welcome message, ice breakers or commands",
        "description": "Updates a number's conversational automation. Meta's own write replaces the whole object, so this endpoint reads the current settings, merges your change and writes everything back: fields you omit are kept; a list you send replaces that list. Send at least one of `welcomeMessageEnabled`, `iceBreakers`, `commands`. The response is re-read from Meta after the write.\n\n`welcomeMessageEnabled` sends nothing on its own: it makes WhatsApp emit a `request_welcome` inbound message when a customer opens a brand-new chat, and your integration has to reply to it.\n\nEvery rule is checked before calling Meta and all problems come back at once in `issues`: at most 4 ice breakers of 80 characters, single-line, non-empty and distinct; at most 30 commands, each with a name of at most 32 characters without leading `/` or spaces (unique, case-insensitive) and a non-empty description of at most 256 characters. No cost and no Meta review: changes show in the chat as soon as they are saved.\n\nWhich number: `connectionId` in the body; required with a partner-wide key, defaults to the client's main number with a client-scoped key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConversationSettingsUpdateRequest"
              },
              "examples": {
                "iceBreakers": {
                  "summary": "Replace the ice breakers only",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "iceBreakers": [
                      "See the menu",
                      "Track my order",
                      "Opening hours"
                    ]
                  }
                },
                "commands": {
                  "summary": "Replace the commands",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "commands": [
                      {
                        "name": "menu",
                        "description": "Today's menu and prices"
                      },
                      {
                        "name": "order",
                        "description": "Status of your last order"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings as Meta now holds them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationSettingsResponse"
                },
                "examples": {
                  "settings": {
                    "summary": "Settings",
                    "value": {
                      "data": {
                        "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                        "phoneNumberId": "106540352242922",
                        "welcomeMessageEnabled": true,
                        "iceBreakers": [
                          "See the menu",
                          "Track my order",
                          "Opening hours"
                        ],
                        "commands": [
                          {
                            "name": "menu",
                            "description": "Today's menu and prices"
                          },
                          {
                            "name": "order",
                            "description": "Status of your last order"
                          }
                        ]
                      },
                      "limits": {
                        "iceBreakersMax": 4,
                        "iceBreakerMax": 80,
                        "commandsMax": 30,
                        "commandNameMax": 32,
                        "commandDescriptionMax": 256
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_conversation_settings` with every problem in `issues` (this body is ConversationSettingsValidationError). Also `connection_required` and `nothing_to_update` (no settings field sent), with the plain Error body.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ConversationSettingsValidationError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "issues": {
                    "summary": "Two problems",
                    "value": {
                      "error": "invalid_conversation_settings",
                      "message": "2 problem(s) would be visible in your customers' chats",
                      "issues": [
                        {
                          "field": "iceBreakers[1]",
                          "message": "is empty — it would render as a blank tappable chip."
                        },
                        {
                          "field": "commands[0].name",
                          "message": "must not start with \"/\" — WhatsApp adds it, so \"/menu\" would show as \"//menu\"."
                        }
                      ],
                      "limits": {
                        "iceBreakersMax": 4,
                        "iceBreakerMax": 80,
                        "commandsMax": 30,
                        "commandNameMax": 32,
                        "commandDescriptionMax": 256
                      }
                    }
                  },
                  "nothing": {
                    "summary": "No field to update",
                    "value": {
                      "error": "nothing_to_update",
                      "message": "Send at least one of welcomeMessageEnabled, iceBreakers or commands"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/qr-codes": {
      "get": {
        "operationId": "listQrCodes",
        "tags": [
          "QR codes"
        ],
        "summary": "List a number's QR codes and short links",
        "description": "Lists every QR code / short link of a number, read live from Meta (up to 2,000 per number, in one page). Each is a permanent `https://wa.me/message/{code}` link that opens WhatsApp with `prefilledMessage` typed but not sent. Meta reports no analytics for QR codes and short links (no scans, clicks or referrers): to measure a campaign, point the printed code at a redirect you host that counts hits and forwards to `deepLinkUrl`.\n\nWhich number: `connectionId` (the Genuka connection `id` from `GET /api/v1/connections` or `GET /api/v1/numbers`). It is required with a partner-wide key (`400 connection_required` otherwise); with a client-scoped key it may be omitted and defaults to the client's main number (its oldest number if none is marked main).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The codes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrCodeListResponse"
                },
                "examples": {
                  "list": {
                    "summary": "One code",
                    "value": {
                      "data": [
                        {
                          "code": "ABCDEFGHIJKL1",
                          "prefilledMessage": "Bonjour, je veux voir le menu",
                          "deepLinkUrl": "https://wa.me/message/ABCDEFGHIJKL1"
                        }
                      ],
                      "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                      "limits": {
                        "prefilledMessageMax": 140,
                        "perNumberMax": 2000,
                        "analytics": "none — Meta reports no scan or click data for QR codes and short links"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`: a partner-wide key must pass `connectionId`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createQrCode",
        "tags": [
          "QR codes"
        ],
        "summary": "Create a QR code / short link",
        "description": "Creates a permanent short link (`https://wa.me/message/{code}`) and optionally its QR image, for flyers, receipts, packaging or a shop window. Opening it starts a chat with the number with `prefilledMessage` already typed; once the customer sends it, the 24-hour customer service window is open and free-form replies are allowed. The prefilled text doubles as a routing key (\"menu\", \"order status\"), so it is required and must not be blank (at most 140 characters).\n\n`imageFormat` (`SVG` or `PNG`) adds `qrImageUrl`, a URL that expires: download the image instead of printing the URL. Meta reports no analytics for QR codes and short links (no scans, clicks or referrers): to measure a campaign, point the printed code at a redirect you host that counts hits and forwards to `deepLinkUrl`.\n\nWhich number: `connectionId` in the body; required with a partner-wide key, defaults to the client's main number with a client-scoped key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QrCodeCreateRequest"
              },
              "examples": {
                "withImage": {
                  "summary": "Short link + PNG",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "prefilledMessage": "Bonjour, je veux voir le menu",
                    "imageFormat": "PNG"
                  }
                },
                "linkOnly": {
                  "summary": "Short link only",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "prefilledMessage": "Where is my order?"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrCodeCreateResponse"
                },
                "examples": {
                  "created": {
                    "summary": "Created with image",
                    "value": {
                      "data": {
                        "code": "ABCDEFGHIJKL1",
                        "prefilledMessage": "Bonjour, je veux voir le menu",
                        "deepLinkUrl": "https://wa.me/message/ABCDEFGHIJKL1",
                        "qrImageUrl": "https://scontent.whatsapp.net/v/t39.8562-34/qr_ABCDEFGHIJKL1.png"
                      },
                      "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                      "limits": {
                        "prefilledMessageMax": 140,
                        "perNumberMax": 2000,
                        "analytics": "none — Meta reports no scan or click data for QR codes and short links"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`missing_fields` (no `prefilledMessage`), `connection_required`, or `invalid_qr_code` (blank text, more than 140 characters, or `imageFormat` other than SVG/PNG; `message` says which)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/qr-codes/{code}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/QrCodePath"
        }
      ],
      "get": {
        "operationId": "getQrCode",
        "tags": [
          "QR codes"
        ],
        "summary": "Get one QR code",
        "description": "Reads one QR code / short link of a number from Meta.\n\nWhich number: `connectionId` (the Genuka connection `id` from `GET /api/v1/connections` or `GET /api/v1/numbers`). It is required with a partner-wide key (`400 connection_required` otherwise); with a client-scoped key it may be omitted and defaults to the client's main number (its oldest number if none is marked main).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrCodeResponse"
                },
                "examples": {
                  "one": {
                    "summary": "Code",
                    "value": {
                      "data": {
                        "code": "ABCDEFGHIJKL1",
                        "prefilledMessage": "Bonjour, je veux voir le menu",
                        "deepLinkUrl": "https://wa.me/message/ABCDEFGHIJKL1"
                      },
                      "connectionId": "cm8x4k9r20003qz7hb2c6e8f4"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`qr_code_not_found` (no such code on this number) or `connection_not_found`."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateQrCode",
        "tags": [
          "QR codes"
        ],
        "summary": "Change the message behind a QR code",
        "description": "Changes the prefilled message behind an existing code without changing the code, so every copy already printed keeps working and now opens with the new text. `imageFormat` optionally returns a fresh `qrImageUrl`.\n\nWhich number: `connectionId` in the body; required with a partner-wide key, defaults to the client's main number with a client-scoped key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QrCodeUpdateRequest"
              },
              "examples": {
                "retarget": {
                  "summary": "New text, same code",
                  "value": {
                    "connectionId": "cm8x4k9r20003qz7hb2c6e8f4",
                    "prefilledMessage": "Bonjour, je veux le menu du soir"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrCodeResponse"
                },
                "examples": {
                  "updated": {
                    "summary": "Updated",
                    "value": {
                      "data": {
                        "code": "ABCDEFGHIJKL1",
                        "prefilledMessage": "Bonjour, je veux le menu du soir",
                        "deepLinkUrl": "https://wa.me/message/ABCDEFGHIJKL1"
                      },
                      "connectionId": "cm8x4k9r20003qz7hb2c6e8f4"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`missing_fields` (no `prefilledMessage`), `connection_required`, or `invalid_qr_code`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteQrCode",
        "tags": [
          "QR codes"
        ],
        "summary": "Delete a QR code (irreversible)",
        "description": "Deletes a QR code / short link. Irreversible: the link stops working immediately, every printed copy leads nowhere, and the same code is never issued again. To change what a code says, use `PATCH` instead.\n\nWhich number: `connectionId` in the query string (required with a partner-wide key; defaults to the client's main number with a client-scoped key).",
        "parameters": [
          {
            "$ref": "#/components/parameters/ManagedConnectionIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QrCodeDeleteResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "Deleted",
                    "value": {
                      "ok": true,
                      "warning": "Every printed asset pointing at ABCDEFGHIJKL1 now leads nowhere. The code cannot be re-issued."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError",
            "description": "`connection_required`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden",
            "description": "`account_deactivated`: the client owning this number, or the key's own client, is suspended. Also `meta_error` with `meta.errorClass: \"config\"` when Meta refused the call for a token, permission or registration reason that no change to the request can fix."
          },
          "404": {
            "$ref": "#/components/responses/NotFound",
            "description": "`connection_not_found`: no connection with this id within the key's scope (a client-scoped key cannot see another client's numbers)."
          },
          "422": {
            "description": "Meta read the request and refused it (`meta_error`). The body carries Meta's own diagnosis under `meta`: `errorClass`, `retryable`, `code`, `subcode`, `details` and `traceId` (quote `traceId` verbatim to Meta support). Fix the request; retrying it unchanged will fail again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "refused": {
                    "summary": "Graph refused the call",
                    "value": {
                      "error": "meta_error",
                      "message": "(#100) Invalid parameter",
                      "meta": {
                        "errorClass": "unknown",
                        "retryable": false,
                        "code": 100,
                        "traceId": "AbC1dEfGhIjK"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "Graph was unreachable, failing, or rate-limiting (`meta_error`, `meta.retryable` usually true). Not caused by the request: retry later with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream": {
                    "summary": "Graph unavailable",
                    "value": {
                      "error": "meta_error",
                      "message": "Graph API request failed with status 503",
                      "meta": {
                        "errorClass": "retryable",
                        "retryable": true
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "event": {
      "post": {
        "operationId": "receiveWebhookEvent",
        "tags": [
          "Webhooks"
        ],
        "summary": "Event delivered to your endpoint",
        "description": "What Genuka WA POSTs to every endpoint registered with `POST /api/v1/webhooks`: one event per request, as JSON. This is how replies, inbound messages and delivery statuses reach you; the REST API has no endpoint to poll for them.\n\n**Verify the signature** on the raw body before parsing it. `X-Genuka-Signature` is `t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `<t>.<raw body>` keyed with the endpoint's secret (`whsec_…`, returned when the endpoint is created or its secret rotated). Compare in constant time and reject a `t` more than 300 seconds away from your clock.\n\n**De-duplicate** on `X-Genuka-Delivery` (also the body's `id`): it is stable across retries and replays.\n\n**Answer any 2xx within 10 seconds.** Anything else, a timeout or a redirect (redirects are not followed) is a failed attempt, retried after 1 min, 5 min, 30 min, 2 h and 6 h (6 attempts in all), then marked `failed`; a failed delivery can be replayed with `POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay`.\n\nFor Meta's original fields (`messages`, `message_template_status_update`, `account_update`, `phone_number_quality_update`) `type` is a legacy name (`inbound_message`, `message_status`, `template_status`, `account_update`, `phone_quality`) and `data` is Meta's raw object. A `message_status` event is a delivery receipt (`sent`, `delivered`, `read`, `failed`) keyed by the `messageId` that `POST /api/v1/messages` returned. Ignore fields you do not know: payloads may gain fields.",
        "security": [],
        "parameters": [
          {
            "name": "X-Genuka-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\">`, keyed with the endpoint secret.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791381731,v1=5f0c3a8e2b9d4c7a1e6f0b3d8c2a5e9f7b1d4c6a8e0f2b5d7c9a1e3f5b7d9c0a"
          },
          {
            "name": "X-Genuka-Event",
            "in": "header",
            "required": true,
            "description": "Envelope `type` of this event.",
            "schema": {
              "type": "string"
            },
            "example": "message_status"
          },
          {
            "name": "X-Genuka-Event-Field",
            "in": "header",
            "required": true,
            "description": "Meta webhook field the event came from.",
            "schema": {
              "type": "string"
            },
            "example": "messages"
          },
          {
            "name": "X-Genuka-Delivery",
            "in": "header",
            "required": true,
            "description": "Delivery id, identical to the body's `id`. Stable across retries and replays: de-duplicate on it.",
            "schema": {
              "type": "string"
            },
            "example": "cm8y2d7f90015ab4c5u3v8n2k"
          },
          {
            "name": "X-Genuka-Webhook-Id",
            "in": "header",
            "required": true,
            "description": "Id of the endpoint this delivery targets.",
            "schema": {
              "type": "string"
            },
            "example": "cm8y1w3e50002ab4cx7z0q9r1"
          },
          {
            "name": "X-Genuka-Attempt",
            "in": "header",
            "required": true,
            "description": "Attempt number, starting at 1.",
            "schema": {
              "type": "string"
            },
            "example": "1"
          },
          {
            "name": "X-Genuka-Test",
            "in": "header",
            "required": false,
            "description": "`true` on the synthetic events sent by `POST /api/v1/webhooks/{id}/test`: skip side effects.",
            "schema": {
              "type": "string"
            },
            "example": "true"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEventEnvelope"
              },
              "examples": {
                "delivered": {
                  "summary": "Delivery receipt",
                  "value": {
                    "id": "cm8y2d7f90015ab4c5u3v8n2k",
                    "type": "message_status",
                    "field": "messages",
                    "created_at": "2026-10-07T14:02:11.408Z",
                    "partner_id": "cm8w0p2t10000xy9z8a7b6c5d",
                    "company_id": "cm8x4k2p10000qz7h5n3d9w1a",
                    "connection_id": "cm8x4k9r20003qz7hb2c6e8f4",
                    "waba_id": "102290129340398",
                    "phone_number_id": "106540352242922",
                    "data": {
                      "id": "wamid.HBgLMjM3Njk5MDAxMTIyFQIAERgSQzVGOEI4",
                      "status": "delivered",
                      "timestamp": "1791381731",
                      "recipient_id": "237699001122"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged. Any 2xx answered within 10 seconds counts as delivered; the body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect or a timeout is a failed attempt and is retried."
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "CompanyIdFilter": {
        "name": "companyId",
        "in": "query",
        "required": false,
        "description": "Restrict the result to one client (company, from `GET /api/v1/companies`). With a client-scoped key it defaults to that client, and naming another client answers 403 `out_of_scope`. With a partner-wide key, omit it to cover every client.",
        "schema": {
          "type": "string"
        },
        "example": "cmp_1"
      },
      "MediaIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The asset id returned by `POST /api/v1/media`.",
        "schema": {
          "type": "string"
        },
        "example": "ast_1"
      },
      "TemplateIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Genuka template id (`id` from `GET /api/v1/templates`), not Meta's `providerId`.",
        "schema": {
          "type": "string"
        },
        "example": "tpl_123"
      },
      "FlowIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Genuka Flow id (`id` from `GET /api/v1/flows`), not Meta's `providerId`.",
        "schema": {
          "type": "string"
        },
        "example": "flw_1"
      },
      "CampaignIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Campaign id returned by `POST /api/v1/campaigns`.",
        "schema": {
          "type": "string"
        },
        "example": "cpg_9"
      },
      "ManagedConnectionIdQuery": {
        "name": "connectionId",
        "in": "query",
        "required": false,
        "description": "Genuka connection id of the number to act on. Required with a partner-wide key; defaults to the client's main number with a client-scoped key. A number outside the key's scope answers `404 connection_not_found`.",
        "schema": {
          "type": "string"
        },
        "example": "cm8x4k9r20003qz7hb2c6e8f4"
      },
      "NumberConnectionIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Genuka connection id (`id` from `GET /api/v1/numbers` or `GET /api/v1/connections`), not Meta's phone number id.",
        "schema": {
          "type": "string"
        },
        "example": "cm8x4k9r20003qz7hb2c6e8f4"
      },
      "WebhookIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Webhook endpoint id.",
        "schema": {
          "type": "string"
        },
        "example": "cm8y1w3e50002ab4cx7z0q9r1"
      },
      "WebhookDeliveryIdPath": {
        "name": "deliveryId",
        "in": "path",
        "required": true,
        "description": "Delivery id (from `GET /api/v1/webhooks/{id}/deliveries`, or the `X-Genuka-Delivery` header you received).",
        "schema": {
          "type": "string"
        },
        "example": "cm8y2d7f90015ab4c5u3v8n2k"
      },
      "CompanyIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Client (company) id.",
        "schema": {
          "type": "string"
        },
        "example": "cm8x4k2p10000qz7h5n3d9w1a"
      },
      "InvoiceIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Invoice id (not its human `number`).",
        "schema": {
          "type": "string"
        },
        "example": "cm8z0i4n30001de6f2g8h5j7l"
      },
      "QrCodePath": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "The QR code's `code` (the tail of its `https://wa.me/message/{code}` link).",
        "schema": {
          "type": "string"
        },
        "example": "ABCDEFGHIJKL1"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The body of every failure this API produces. Branch on `error` (a stable, machine-readable code); `message` is prose for humans and may change. When Meta refused the call, `meta` carries Meta's own diagnosis. A few endpoints add extra fields documented on the endpoint itself (for example `results` on a failed template sync).\n\nTwo failures do not come from the API and carry no JSON: the CDN in front of it answers `403` with the plain-text body `error code: 1010` to a few bot User-Agents (send a descriptive `User-Agent`), and replaces the body of any origin 5xx with its own page.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code, snake_case. Examples: `missing_fields`, `invalid_token`, `connection_not_found`, `plan_limit_messages`, `meta_rejected`, `send_needs_template`.",
            "examples": [
              "missing_fields"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. Absent on some codes (`missing_bearer_token`, `invalid_token`, most `*_not_found`)."
          },
          "meta": {
            "$ref": "#/components/schemas/ErrorMetaDetails"
          }
        },
        "additionalProperties": true
      },
      "ErrorMetaDetails": {
        "type": "object",
        "description": "Meta's own identifiers for a refusal coming from the WhatsApp Cloud API / Graph API, echoed verbatim. Present when the failure came from Meta (or from the `@genuka/whatsapp` validators that classify errors the same way).",
        "required": [
          "errorClass",
          "retryable"
        ],
        "properties": {
          "errorClass": {
            "type": "string",
            "enum": [
              "validation",
              "retryable",
              "needs_template",
              "recipient_permanent",
              "recipient_throttled",
              "media",
              "template",
              "config",
              "unknown"
            ],
            "description": "What to do about it. `needs_template`: the 24-hour customer service window is closed, resend as an approved template (Meta 131047). `recipient_permanent`: the recipient cannot be reached or opted out (131026, 131050), do not retry. `recipient_throttled`: per-user marketing cap (131049), retry much later. `retryable`: throttling or a Meta-side failure (4, 130429, 5xx), retry with backoff. `media`: media could not be fetched or uploaded (131052, 131053), re-upload and retry once. `template`: the template is wrong, paused, disabled or missing in that language (132xxx). `config`: token, permission or registration problem (0, 3, 10, 190, 368, 133010), no retry will help. `validation` / `unknown`: fix the payload."
          },
          "retryable": {
            "type": "boolean",
            "description": "True when retrying the exact same request can plausibly succeed (`retryable` and `media` classes)."
          },
          "code": {
            "type": "integer",
            "description": "Meta's numeric error code, e.g. 131047 or 100."
          },
          "subcode": {
            "type": "integer",
            "description": "Meta's `error_subcode`, when it sent one."
          },
          "details": {
            "type": "string",
            "description": "Meta's `error_data.details`: usually the most specific explanation available."
          },
          "traceId": {
            "type": "string",
            "description": "Meta's `fbtrace_id`. Quote it verbatim when opening a ticket with Meta support."
          }
        }
      },
      "MessageMedia": {
        "type": "object",
        "description": "A media reference. Give exactly one of `assetId` (recommended), `id` or `link`. `assetId` is a file uploaded with `POST /api/v1/media`: Genuka uploads the bytes to Meta for the sending number at send time, so the file is never exposed on a public URL and never expires under you. `id` is a Meta `media_id` you minted yourself (valid for one phone number, 30 days). `link` is a public HTTPS URL Meta downloads (Meta caches a link for a while; reuse the same URL for the same file).",
        "properties": {
          "assetId": {
            "type": "string",
            "description": "Id of a file uploaded with `POST /api/v1/media` (kept 30 days). Do not combine with `id`.",
            "examples": [
              "ast_1"
            ]
          },
          "id": {
            "type": "string",
            "description": "A Meta `media_id` uploaded for this same phone number."
          },
          "link": {
            "type": "string",
            "format": "uri",
            "description": "Public URL Meta fetches."
          },
          "caption": {
            "type": "string",
            "maxLength": 1024,
            "description": "image, video and document only; ignored on audio and sticker."
          },
          "filename": {
            "type": "string",
            "description": "document only: the file name shown to the recipient."
          }
        }
      },
      "MessageLocation": {
        "type": "object",
        "required": [
          "latitude",
          "longitude"
        ],
        "properties": {
          "latitude": {
            "type": [
              "number",
              "string"
            ],
            "examples": [
              4.0511
            ]
          },
          "longitude": {
            "type": [
              "number",
              "string"
            ],
            "examples": [
              9.7679
            ]
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          }
        }
      },
      "MessageInteractiveHeader": {
        "description": "Header of an interactive message: a plain string (text header) or one media object.",
        "oneOf": [
          {
            "type": "string",
            "maxLength": 60
          },
          {
            "type": "object",
            "required": [
              "image"
            ],
            "properties": {
              "image": {
                "$ref": "#/components/schemas/MessageMedia"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "video"
            ],
            "properties": {
              "video": {
                "$ref": "#/components/schemas/MessageMedia"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "document"
            ],
            "properties": {
              "document": {
                "$ref": "#/components/schemas/MessageMedia"
              }
            }
          }
        ]
      },
      "MessageTemplateParamValue": {
        "description": "A template parameter. A string or number is sent as a `text` parameter. An object carrying a `type` field (Meta's `currency`, `date_time`, …) is forwarded to Meta untouched.",
        "oneOf": [
          {
            "type": "string"
          },
          {
            "type": "number"
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string"
              }
            },
            "additionalProperties": true
          }
        ]
      },
      "MessageTemplateHeader": {
        "description": "Header parameters for a template whose header has a variable or media: `{ text }` fills a TEXT header variable, `{ image | video | document }` supplies the media at send time (the header_handle used at creation is not reused), `{ location }` a LOCATION header.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "text"
            ],
            "properties": {
              "text": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/MessageTemplateParamValue"
                  },
                  {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/MessageTemplateParamValue"
                    }
                  }
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "image"
            ],
            "properties": {
              "image": {
                "$ref": "#/components/schemas/MessageMedia"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "video"
            ],
            "properties": {
              "video": {
                "$ref": "#/components/schemas/MessageMedia"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "document"
            ],
            "properties": {
              "document": {
                "$ref": "#/components/schemas/MessageMedia"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "location"
            ],
            "properties": {
              "location": {
                "$ref": "#/components/schemas/MessageLocation"
              }
            }
          }
        ]
      },
      "MessageTemplateButtonUrl": {
        "type": "object",
        "required": [
          "type",
          "text"
        ],
        "description": "Fills the dynamic suffix of a URL button (the `{{1}}` in `https://example.com/track/{{1}}`).",
        "properties": {
          "type": {
            "const": "url"
          },
          "text": {
            "type": "string",
            "examples": [
              "1024"
            ]
          },
          "index": {
            "type": "integer",
            "minimum": 0,
            "description": "Position of the button in the template. Defaults to its position in this array."
          }
        }
      },
      "MessageTemplateButtonQuickReply": {
        "type": "object",
        "required": [
          "type",
          "payload"
        ],
        "description": "Payload returned on the webhook when the customer taps this quick-reply button.",
        "properties": {
          "type": {
            "const": "quick_reply"
          },
          "payload": {
            "type": "string"
          },
          "index": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "MessageTemplateButtonCopyCode": {
        "type": "object",
        "required": [
          "type",
          "code"
        ],
        "description": "Coupon code for a COPY_CODE button.",
        "properties": {
          "type": {
            "const": "copy_code"
          },
          "code": {
            "type": "string",
            "examples": [
              "SAVE20"
            ]
          },
          "index": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "MessageTemplateButtonOtp": {
        "type": "object",
        "required": [
          "type",
          "code"
        ],
        "description": "Authentication-template OTP button (sent as a URL button carrying the code). Prefer the `otp` shorthand on the template object, which fills body and button at once.",
        "properties": {
          "type": {
            "const": "otp"
          },
          "code": {
            "type": "string"
          },
          "index": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "MessageTemplateButton": {
        "type": "object",
        "oneOf": [
          {
            "$ref": "#/components/schemas/MessageTemplateButtonUrl"
          },
          {
            "$ref": "#/components/schemas/MessageTemplateButtonQuickReply"
          },
          {
            "$ref": "#/components/schemas/MessageTemplateButtonCopyCode"
          },
          {
            "$ref": "#/components/schemas/MessageTemplateButtonOtp"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "url": "#/components/schemas/MessageTemplateButtonUrl",
            "quick_reply": "#/components/schemas/MessageTemplateButtonQuickReply",
            "copy_code": "#/components/schemas/MessageTemplateButtonCopyCode",
            "otp": "#/components/schemas/MessageTemplateButtonOtp"
          }
        }
      },
      "MessageTemplateParams": {
        "type": "object",
        "required": [
          "name"
        ],
        "description": "Which approved template to send and how to fill its variables. Body parameters go in `body` (or its alias `variables`) for positional `{{1}}` templates, or `bodyNamed` for templates created with `parameterFormat: NAMED`. `components` is an escape hatch: a full Meta `components` array forwarded verbatim (everything else except `name` and `language` is then ignored) — use it for anything the shorthand does not cover, such as a FLOW button.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Template name exactly as created (lowercase, digits, underscores).",
            "examples": [
              "order_shipped"
            ]
          },
          "language": {
            "type": "string",
            "default": "en_US",
            "description": "Language code of the approved template, e.g. `en_US`, `fr`. Defaults to `en_US` when omitted, while template creation defaults to `en`: always pass the template's exact language.",
            "examples": [
              "en_US"
            ]
          },
          "body": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MessageTemplateParamValue"
            },
            "description": "Positional body parameters, in order."
          },
          "variables": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MessageTemplateParamValue"
            },
            "description": "Alias of `body`, kept for backwards compatibility. Used only when `body` is absent."
          },
          "bodyNamed": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/MessageTemplateParamValue"
            },
            "description": "Named body parameters, for templates created with `parameterFormat: NAMED`.",
            "examples": [
              {
                "customer_name": "Alice",
                "date": "June 20"
              }
            ]
          },
          "header": {
            "$ref": "#/components/schemas/MessageTemplateHeader"
          },
          "buttons": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MessageTemplateButton"
            },
            "description": "Dynamic button parameters."
          },
          "otp": {
            "type": "string",
            "description": "AUTHENTICATION templates only: the one-time code. Fills the body and the OTP button (index 0) as Meta requires.",
            "examples": [
              "472913"
            ]
          },
          "components": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "Escape hatch: a fully-formed Meta send-side `components` array, forwarded untouched."
          }
        }
      },
      "MessageReplyButton": {
        "type": "object",
        "required": [
          "id",
          "title"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 256,
            "description": "Returned on the webhook when tapped."
          },
          "title": {
            "type": "string",
            "maxLength": 20
          }
        }
      },
      "MessageListSection": {
        "type": "object",
        "required": [
          "rows"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 24
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "title"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string",
                  "maxLength": 24
                },
                "description": {
                  "type": "string",
                  "maxLength": 72
                }
              }
            }
          }
        }
      },
      "MessageSendRequest": {
        "type": "object",
        "required": [
          "connectionId",
          "to"
        ],
        "description": "`connectionId` and `to`, plus one content field. Only `template` can reach a customer outside the 24-hour customer service window; every other content field is a free-form session message. If several content fields are present only one is sent (`template` first, then `raw`, then `text`, media, `location`, `contacts`, `reaction`, `buttons`, `list`, `cta`, `locationRequest`).",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "The sending WhatsApp number (a connection id from `GET /api/v1/connections`).",
            "examples": [
              "con_1"
            ]
          },
          "to": {
            "type": "string",
            "description": "Recipient phone number in international format with country code, e.g. `+237690000001`. Must not be the sending number itself.",
            "examples": [
              "+237690000001"
            ]
          },
          "replyTo": {
            "type": "string",
            "description": "wamid of an inbound message to quote (reply in thread)."
          },
          "template": {
            "$ref": "#/components/schemas/MessageTemplateParams"
          },
          "text": {
            "description": "Text message: a string, or `{ body, previewUrl }` to render a link preview.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 4096
              },
              {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 4096
                  },
                  "previewUrl": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            ]
          },
          "image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MessageMedia"
              }
            ],
            "description": "JPEG or PNG, up to 5 MB."
          },
          "video": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MessageMedia"
              }
            ],
            "description": "MP4 or 3GPP, up to 16 MB."
          },
          "audio": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MessageMedia"
              }
            ],
            "description": "AAC, AMR, MP3, M4A or OGG, up to 16 MB. No caption."
          },
          "document": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MessageMedia"
              }
            ],
            "description": "PDF, DOC(X), XLS(X), PPT(X) or TXT, up to 100 MB."
          },
          "sticker": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MessageMedia"
              }
            ],
            "description": "WebP, up to 100 KB static or 500 KB animated."
          },
          "location": {
            "$ref": "#/components/schemas/MessageLocation"
          },
          "contacts": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "Meta contact card objects, forwarded verbatim."
          },
          "reaction": {
            "type": "object",
            "required": [
              "messageId"
            ],
            "properties": {
              "messageId": {
                "type": "string",
                "description": "wamid of the message to react to."
              },
              "emoji": {
                "type": "string",
                "description": "The emoji. An empty string removes the reaction."
              }
            }
          },
          "buttons": {
            "type": "object",
            "required": [
              "body",
              "buttons"
            ],
            "description": "Interactive reply buttons (up to 3). The tapped button's `id` comes back on the webhook.",
            "properties": {
              "body": {
                "type": "string",
                "maxLength": 1024
              },
              "header": {
                "$ref": "#/components/schemas/MessageInteractiveHeader"
              },
              "footer": {
                "type": "string",
                "maxLength": 60
              },
              "buttons": {
                "type": "array",
                "minItems": 1,
                "maxItems": 3,
                "items": {
                  "$ref": "#/components/schemas/MessageReplyButton"
                }
              }
            }
          },
          "list": {
            "type": "object",
            "required": [
              "body",
              "button",
              "sections"
            ],
            "description": "Interactive list menu: up to 10 sections and 10 rows in total. The picked row's `id` comes back on the webhook.",
            "properties": {
              "body": {
                "type": "string",
                "maxLength": 1024
              },
              "header": {
                "type": "string",
                "maxLength": 60,
                "description": "Text header only."
              },
              "footer": {
                "type": "string",
                "maxLength": 60
              },
              "button": {
                "type": "string",
                "maxLength": 20,
                "description": "Label of the button that opens the list."
              },
              "sections": {
                "type": "array",
                "minItems": 1,
                "maxItems": 10,
                "items": {
                  "$ref": "#/components/schemas/MessageListSection"
                }
              }
            }
          },
          "cta": {
            "type": "object",
            "required": [
              "body",
              "url",
              "displayText"
            ],
            "description": "A single call-to-action button opening a URL.",
            "properties": {
              "body": {
                "type": "string",
                "maxLength": 1024
              },
              "header": {
                "$ref": "#/components/schemas/MessageInteractiveHeader"
              },
              "footer": {
                "type": "string",
                "maxLength": 60
              },
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2000
              },
              "displayText": {
                "type": "string",
                "maxLength": 20
              }
            }
          },
          "locationRequest": {
            "type": "object",
            "required": [
              "body"
            ],
            "description": "Asks the customer to share their location (a Send location button).",
            "properties": {
              "body": {
                "type": "string",
                "maxLength": 1024
              }
            }
          },
          "raw": {
            "type": "object",
            "additionalProperties": true,
            "description": "Escape hatch: a fully-formed Cloud API type fragment (for example `{ \"type\": \"interactive\", \"interactive\": { \"type\": \"flow\", … } }` to send a published Flow). Not validated by Genuka; Meta's errors come back as-is."
          }
        },
        "anyOf": [
          {
            "required": [
              "template"
            ]
          },
          {
            "required": [
              "text"
            ]
          },
          {
            "required": [
              "image"
            ]
          },
          {
            "required": [
              "video"
            ]
          },
          {
            "required": [
              "audio"
            ]
          },
          {
            "required": [
              "document"
            ]
          },
          {
            "required": [
              "sticker"
            ]
          },
          {
            "required": [
              "location"
            ]
          },
          {
            "required": [
              "contacts"
            ]
          },
          {
            "required": [
              "reaction"
            ]
          },
          {
            "required": [
              "buttons"
            ]
          },
          {
            "required": [
              "list"
            ]
          },
          {
            "required": [
              "cta"
            ]
          },
          {
            "required": [
              "locationRequest"
            ]
          },
          {
            "required": [
              "raw"
            ]
          }
        ]
      },
      "MessageWindowNotice": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "description": "A non-blocking caveat on a free-form send. Meta accepts a free-form message sent outside the 24-hour customer service window with a valid message id and then drops it, so the send went out but will most likely not be delivered. Resend as an approved template.",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "outside_service_window",
              "unverified_service_window"
            ],
            "description": "`outside_service_window`: the contact's last inbound message is older than 24 hours. `unverified_service_window`: no inbound message from this contact is on record, so the window could not be verified."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "MessageSendResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "messageId"
            ],
            "properties": {
              "messageId": {
                "type": "string",
                "description": "Meta's message id (wamid). Delivery statuses (`sent`, `delivered`, `read`, `failed`) arrive on your webhooks under this id.",
                "examples": [
                  "wamid.HBgMMjM3NjkwMDAwMDAxFQIAERgSQjc1"
                ]
              },
              "warning": {
                "$ref": "#/components/schemas/MessageWindowNotice"
              }
            }
          }
        }
      },
      "MessageReadRequest": {
        "type": "object",
        "required": [
          "connectionId"
        ],
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "The connection (number) that received the message.",
            "examples": [
              "con_1"
            ]
          },
          "typing": {
            "type": "boolean",
            "default": false,
            "description": "Also show the typing indicator. It cannot be shown without marking the message read, disappears after 25 seconds or when you reply, and cannot be cancelled."
          }
        }
      },
      "MessageReadResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "success",
              "messageId",
              "typing"
            ],
            "properties": {
              "success": {
                "type": "boolean"
              },
              "messageId": {
                "type": "string",
                "description": "The inbound wamid from the path."
              },
              "typing": {
                "type": "boolean"
              },
              "typingExpiresInSeconds": {
                "type": "integer",
                "description": "Present when `typing` is true: 25, the indicator's lifetime.",
                "examples": [
                  25
                ]
              }
            }
          }
        }
      },
      "MediaKind": {
        "type": "string",
        "enum": [
          "image",
          "video",
          "audio",
          "document",
          "sticker"
        ],
        "description": "Decided at upload from the MIME type."
      },
      "MediaAsset": {
        "type": "object",
        "description": "A stored file. Send it with `{ \"assetId\": \"<id>\" }` anywhere a media object is accepted (message media, template headers): Genuka mints Meta's `media_id` for the sending number at send time. No Meta identifier is ever exposed here.",
        "required": [
          "id",
          "companyId",
          "url",
          "expiresAt",
          "expiresInMs",
          "sha256",
          "mimeType",
          "kind",
          "sizeBytes",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The `assetId`.",
            "examples": [
              "ast_1"
            ]
          },
          "companyId": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "`GET /api/v1/media/{id}/content` on this API. It requires your API key: it is not a public link."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the file is deleted for good: 30 days after upload."
          },
          "expiresInMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Milliseconds left before deletion, floored at 0."
          },
          "sha256": {
            "type": "string",
            "description": "Content hash, lowercase hex. Identical bytes for the same client are stored once."
          },
          "mimeType": {
            "type": "string",
            "examples": [
              "application/pdf"
            ]
          },
          "kind": {
            "$ref": "#/components/schemas/MediaKind"
          },
          "sizeBytes": {
            "type": "integer"
          },
          "filename": {
            "type": [
              "string",
              "null"
            ]
          },
          "inboundMessageId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when the file was archived from an inbound message rather than uploaded."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MediaStorageUsage": {
        "type": "object",
        "description": "Where the account stands against its plan's storage ceiling, across every client of the partner.",
        "required": [
          "usedBytes",
          "limitBytes",
          "assetCount",
          "retentionDays"
        ],
        "properties": {
          "usedBytes": {
            "type": "integer"
          },
          "limitBytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The plan's ceiling in bytes; null when uncapped (Enterprise)."
          },
          "assetCount": {
            "type": "integer"
          },
          "retentionDays": {
            "type": "integer",
            "description": "Days a file is kept after upload (30).",
            "examples": [
              30
            ]
          }
        }
      },
      "MediaListResponse": {
        "type": "object",
        "required": [
          "data",
          "storage",
          "nextCursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MediaAsset"
            }
          },
          "storage": {
            "$ref": "#/components/schemas/MediaStorageUsage"
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` to get the next page; null on the last page."
          }
        }
      },
      "MediaAssetResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MediaAsset"
          }
        }
      },
      "MediaUploadRequest": {
        "type": "object",
        "required": [
          "file"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "contentMediaType": "application/octet-stream",
            "description": "The file, with its Content-Type set. Accepted: image JPEG/PNG (5 MB), video MP4/3GPP (16 MB), audio AAC/AMR/MP3/M4A/OGG (16 MB), document PDF/DOC/DOCX/XLS/XLSX/PPT/PPTX/TXT (100 MB), sticker WebP (100 KB static, 500 KB animated)."
          },
          "companyId": {
            "type": "string",
            "description": "The client that owns the file. Required with a partner-wide key; implied by a client-scoped key."
          },
          "filename": {
            "type": "string",
            "description": "Overrides the multipart file name."
          }
        }
      },
      "MediaUploadResponse": {
        "type": "object",
        "required": [
          "data",
          "deduplicated"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MediaAsset"
          },
          "deduplicated": {
            "type": "boolean",
            "description": "True when identical bytes were already stored for this client: the existing asset is returned and nothing new is stored."
          }
        }
      },
      "MediaDeleteResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "id",
              "deleted"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "deleted": {
                "const": true
              }
            }
          }
        }
      },
      "MediaHeaderHandleRequest": {
        "type": "object",
        "required": [
          "file"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "contentMediaType": "application/octet-stream",
            "description": "The header sample: image/jpeg, image/png, video/mp4, video/3gpp or application/pdf, at most 4 MB (4194304 bytes)."
          },
          "connectionId": {
            "type": "string",
            "description": "A number of the client. Give this or `companyId` (a client-scoped key needs neither)."
          },
          "companyId": {
            "type": "string",
            "description": "The client; its main number is used. The handle is the same whichever number is picked."
          },
          "filename": {
            "type": "string",
            "description": "Name stored by Meta. Defaults to the multipart file name, else `header`."
          }
        }
      },
      "MediaHeaderHandleResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "handle",
              "filename",
              "mimeType",
              "sizeBytes"
            ],
            "properties": {
              "handle": {
                "type": "string",
                "description": "Meta's resumable-upload handle. Put it in the template's HEADER component as `example.header_handle: [\"<handle>\"]` on `POST /api/v1/templates`. Single use; not sendable.",
                "examples": [
                  "4::YXBwbGljYXRpb24vcGRm"
                ]
              },
              "filename": {
                "type": "string"
              },
              "mimeType": {
                "type": "string"
              },
              "sizeBytes": {
                "type": "integer"
              }
            }
          }
        }
      },
      "TemplateCategory": {
        "type": "string",
        "enum": [
          "MARKETING",
          "UTILITY",
          "AUTHENTICATION"
        ],
        "description": "MARKETING: promotions, re-engagement. UTILITY: transactional updates tied to an action the customer took (order, booking, payment). AUTHENTICATION: one-time codes; Meta allows it only for businesses that passed Meta business verification (or another Meta scaling path)."
      },
      "TemplateStatus": {
        "type": "string",
        "enum": [
          "draft",
          "submitted",
          "pending",
          "approved",
          "rejected",
          "paused",
          "disabled"
        ],
        "description": "Local mirror of Meta's review state, lowercase. Only `approved` can be sent. `submitted`/`pending`: in review (Meta's IN_APPEAL and PENDING_DELETION also read `pending`). `paused`: Meta paused an approved template after negative feedback. `disabled`: permanently disabled by Meta. `rejected`: see `rejectionReason`."
      },
      "TemplateComponent": {
        "type": "object",
        "required": [
          "type"
        ],
        "description": "One component of Meta's template definition, passed verbatim (Meta's own field names: `format`, `text`, `example`, `buttons`, `add_security_recommendation`, `code_expiration_minutes`, …). Any component with variables, and any media header, needs an `example`: `{ \"body_text\": [[\"Alice\", \"#1024\"]] }` for the body, `{ \"header_handle\": [\"<handle from POST /api/v1/media/header-handle>\"] }` for an IMAGE/VIDEO/DOCUMENT header. Buttons: QUICK_REPLY, URL, PHONE_NUMBER, COPY_CODE, FLOW, OTP (up to 10, rules vary by type).",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "HEADER",
              "BODY",
              "FOOTER",
              "BUTTONS",
              "CAROUSEL",
              "LIMITED_TIME_OFFER"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "TEXT",
              "IMAGE",
              "VIDEO",
              "DOCUMENT",
              "LOCATION"
            ],
            "description": "HEADER only."
          },
          "text": {
            "type": "string"
          },
          "example": {
            "type": "object",
            "additionalProperties": true
          },
          "buttons": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "additionalProperties": true
      },
      "TemplateLibraryButtonInput": {
        "description": "Destination for one button of a library template: Meta supplies the words, you supply where the button goes.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "type",
              "url"
            ],
            "properties": {
              "type": {
                "const": "URL"
              },
              "url": {
                "type": "object",
                "required": [
                  "base_url"
                ],
                "properties": {
                  "base_url": {
                    "type": "string"
                  },
                  "url_suffix_example": {
                    "type": "string"
                  }
                }
              }
            }
          },
          {
            "type": "object",
            "required": [
              "type",
              "phone_number"
            ],
            "properties": {
              "type": {
                "const": "PHONE_NUMBER"
              },
              "phone_number": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "const": "QUICK_REPLY"
              },
              "text": {
                "type": "string"
              }
            }
          }
        ]
      },
      "TemplateCreateRequest": {
        "type": "object",
        "required": [
          "connectionId",
          "name"
        ],
        "description": "Either `components` (a custom template) or `libraryTemplateName` (instantiate a Meta library template) is required.",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Any number of the client: the template is created on that number's WhatsApp Business Account and is usable by every number of that account.",
            "examples": [
              "con_1"
            ]
          },
          "name": {
            "type": "string",
            "pattern": "^[a-z0-9_]+$",
            "maxLength": 512,
            "description": "Lowercase letters, digits and underscores only. Unique per client and language.",
            "examples": [
              "order_shipped"
            ]
          },
          "language": {
            "type": "string",
            "default": "en",
            "description": "Meta language code, e.g. `en_US`, `en`, `fr`. Pass it explicitly and reuse it verbatim at send time.",
            "examples": [
              "en_US"
            ]
          },
          "category": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TemplateCategory"
              }
            ],
            "default": "MARKETING",
            "description": "Defaults to MARKETING: pass it explicitly."
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateComponent"
            },
            "description": "Meta's `components` array, verbatim."
          },
          "parameterFormat": {
            "type": "string",
            "enum": [
              "POSITIONAL",
              "NAMED"
            ],
            "description": "POSITIONAL uses `{{1}}`, `{{2}}`; NAMED uses `{{customer_name}}` and needs `body_text_named_params` examples."
          },
          "messageSendTtlSeconds": {
            "type": "integer",
            "description": "Drop the message if it cannot be delivered within this many seconds. For AUTHENTICATION templates Meta allows 30-900 (default 600, 10 minutes); Genuka's validator is stricter and accepts 60-600, unless `validate` is false."
          },
          "allowCategoryChange": {
            "type": "boolean",
            "description": "Let Meta recategorize the template instead of rejecting it when it disagrees with `category`."
          },
          "validate": {
            "type": "boolean",
            "default": true,
            "description": "Set to false to skip Genuka's local validator (stricter than Meta's) and let Meta judge the components alone."
          },
          "libraryTemplateName": {
            "type": "string",
            "description": "Name of a Meta library template (from `GET /api/v1/templates/library`). Library templates are approved immediately; `components` is then not needed.",
            "examples": [
              "order_confirmation_1"
            ]
          },
          "libraryTemplateButtonInputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateLibraryButtonInput"
            }
          }
        }
      },
      "TemplateUpdateRequest": {
        "type": "object",
        "description": "At least one of `components`, `category` or `messageSendTtlSeconds`. Name and language are immutable.",
        "properties": {
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateComponent"
            },
            "description": "The complete new components array (not a patch)."
          },
          "category": {
            "$ref": "#/components/schemas/TemplateCategory"
          },
          "messageSendTtlSeconds": {
            "type": "integer"
          },
          "validate": {
            "type": "boolean",
            "default": true,
            "description": "false skips Genuka's local validator."
          }
        }
      },
      "Template": {
        "type": "object",
        "required": [
          "id",
          "companyId",
          "wabaId",
          "name",
          "language",
          "category",
          "components",
          "status",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Genuka template id: use it as `templateId` for campaigns.",
            "examples": [
              "tpl_123"
            ]
          },
          "companyId": {
            "type": "string"
          },
          "wabaId": {
            "type": "string",
            "description": "Meta WhatsApp Business Account id that holds the template."
          },
          "name": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "description": "MARKETING, UTILITY or AUTHENTICATION (Meta may recategorize)."
          },
          "components": {
            "description": "Meta's components array. For a library template it is read back from Meta after creation; if that read failed it is `{ library_template_name, library_template_button_inputs }`.",
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TemplateComponent"
                }
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ]
          },
          "status": {
            "$ref": "#/components/schemas/TemplateStatus"
          },
          "providerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta's template id."
          },
          "rejectionReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta's reason, when rejected."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateQualityScore": {
        "type": "object",
        "description": "Meta's quality score object.",
        "properties": {
          "score": {
            "type": "string",
            "enum": [
              "GREEN",
              "YELLOW",
              "RED",
              "UNKNOWN"
            ]
          },
          "date": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": true
      },
      "TemplateListItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Template"
          },
          {
            "type": "object",
            "required": [
              "qualityScore"
            ],
            "properties": {
              "qualityScore": {
                "description": "Last quality score Meta reported for the template (from its recent status events), or null when none is known.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TemplateQualityScore"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TemplateListResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateListItem"
            }
          }
        }
      },
      "TemplateResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Template"
          }
        }
      },
      "TemplateStatusEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "templateId": {
            "type": "string"
          },
          "fromStatus": {
            "type": [
              "string",
              "null"
            ]
          },
          "toStatus": {
            "type": "string"
          },
          "providerPayload": {
            "description": "Meta's payload that carried the change, when there was one."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateMetaResource": {
        "type": "object",
        "description": "Meta's live view of the template (Graph `message_template` fields, snake_case).",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Meta's uppercase status: APPROVED, PENDING, REJECTED, PAUSED, DISABLED, IN_APPEAL, PENDING_DELETION."
          },
          "sub_category": {
            "type": "string"
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateComponent"
            }
          },
          "quality_score": {
            "$ref": "#/components/schemas/TemplateQualityScore"
          },
          "rejected_reason": {
            "type": "string"
          },
          "parameter_format": {
            "type": "string"
          },
          "message_send_ttl_seconds": {
            "type": "integer"
          },
          "previous_category": {
            "type": "string"
          },
          "correct_category": {
            "type": "string"
          },
          "library_template_name": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "TemplateDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Template"
          },
          {
            "type": "object",
            "required": [
              "statusEvents",
              "qualityScore",
              "meta"
            ],
            "properties": {
              "company": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string"
                  }
                }
              },
              "statusEvents": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TemplateStatusEvent"
                },
                "description": "The 20 most recent status changes, newest first."
              },
              "qualityScore": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TemplateQualityScore"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "meta": {
                "description": "Meta's live view when it could be read; null means Meta could not be asked, not that it has no data.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TemplateMetaResource"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "TemplateDetailResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/TemplateDetail"
          }
        }
      },
      "TemplateDeleteResponse": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true
          }
        }
      },
      "TemplateSyncRequest": {
        "type": "object",
        "description": "Both optional. Empty body: every WhatsApp Business Account the key can reach.",
        "properties": {
          "companyId": {
            "type": "string",
            "description": "Only this client's accounts."
          },
          "connectionId": {
            "type": "string",
            "description": "Only the account of this number. Takes precedence over `companyId`."
          }
        }
      },
      "TemplateSyncWabaResult": {
        "type": "object",
        "required": [
          "wabaId",
          "companyId",
          "ok"
        ],
        "properties": {
          "wabaId": {
            "type": "string"
          },
          "companyId": {
            "type": "string"
          },
          "ok": {
            "type": "boolean"
          },
          "error": {
            "type": "string",
            "description": "Meta's words when this account's read failed."
          }
        }
      },
      "TemplateSyncedRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "companyId": {
            "type": "string"
          },
          "wabaId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/TemplateStatus"
          },
          "providerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "rejectionReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateSyncResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "wabas",
              "synced",
              "failed",
              "templates"
            ],
            "properties": {
              "wabas": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TemplateSyncWabaResult"
                },
                "description": "One entry per WhatsApp Business Account touched."
              },
              "synced": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "templates": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TemplateSyncedRow"
                },
                "description": "The freshly written rows of the accounts that synced."
              }
            }
          }
        }
      },
      "TemplateSyncFailure": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "results": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TemplateSyncWabaResult"
                }
              }
            }
          }
        ]
      },
      "TemplateLibraryEntry": {
        "type": "object",
        "description": "A Meta library template (Graph `message_template_library` fields).",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Pass as `libraryTemplateName` on `POST /api/v1/templates`."
          },
          "body": {
            "type": "string"
          },
          "header": {
            "type": "string"
          },
          "footer": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "language": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "topic": {
            "type": "string"
          },
          "usecase": {
            "type": "string"
          },
          "industry": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "buttons": {
            "type": "array",
            "items": {}
          },
          "body_params": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": true
      },
      "TemplateLibraryResponse": {
        "type": "object",
        "required": [
          "data",
          "paging"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateLibraryEntry"
            }
          },
          "paging": {
            "description": "Meta's cursor paging; pass `paging.cursors.after` as `after` for the next page. Null when Meta sent none.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "cursors": {
                    "type": "object",
                    "properties": {
                      "before": {
                        "type": "string"
                      },
                      "after": {
                        "type": "string"
                      }
                    }
                  },
                  "next": {
                    "type": "string"
                  },
                  "previous": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "TemplateAnalyticsDay": {
        "type": "object",
        "required": [
          "day",
          "template",
          "templateId",
          "sent",
          "delivered",
          "read",
          "clicked",
          "cost",
          "clicksByButton"
        ],
        "properties": {
          "day": {
            "type": "string",
            "format": "date",
            "description": "UTC day, YYYY-MM-DD."
          },
          "template": {
            "type": "string",
            "description": "Template name."
          },
          "templateId": {
            "type": "string",
            "description": "Meta template id the figures came from."
          },
          "sent": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "read": {
            "type": "integer"
          },
          "clicked": {
            "type": "integer"
          },
          "cost": {
            "type": "number",
            "description": "Sum of Meta's COST metric buckets for the day, as reported by Meta."
          },
          "clicksByButton": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string"
                },
                "button_content": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "TemplateAnalyticsResponse": {
        "type": "object",
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateAnalyticsDay"
            },
            "description": "One row per archived day, oldest first."
          },
          "meta": {
            "type": "object",
            "required": [
              "template",
              "granularity",
              "start",
              "end",
              "metaClickRetentionDays",
              "warning"
            ],
            "properties": {
              "template": {
                "type": "string"
              },
              "granularity": {
                "const": "DAILY"
              },
              "start": {
                "type": "string",
                "format": "date-time"
              },
              "end": {
                "type": "string",
                "format": "date-time"
              },
              "metaClickRetentionDays": {
                "type": "integer",
                "description": "Meta purges read and click counters after this many days (7); older figures exist only in Genuka's archive."
              },
              "warning": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Set when Meta returned no data (analytics never enabled for this account: POST this route once) or when the live refresh failed and only the archive is served."
              }
            }
          }
        }
      },
      "TemplateAnalyticsEnableResponse": {
        "type": "object",
        "required": [
          "ok",
          "enabled",
          "wabaId"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "enabled": {
            "const": true
          },
          "wabaId": {
            "type": "string"
          }
        }
      },
      "FlowCategory": {
        "type": "string",
        "enum": [
          "SIGN_UP",
          "SIGN_IN",
          "APPOINTMENT_BOOKING",
          "LEAD_GENERATION",
          "CONTACT_US",
          "CUSTOMER_SUPPORT",
          "SURVEY",
          "OTHER"
        ]
      },
      "FlowStatus": {
        "type": "string",
        "enum": [
          "draft",
          "published",
          "deprecated",
          "blocked",
          "throttled"
        ],
        "description": "Meta's Flow lifecycle, lowercase. `draft`: editable and deletable. `published`: live, can only be deprecated. `deprecated`: retired. `blocked` / `throttled`: set by Meta when the Flow's endpoint is unhealthy."
      },
      "FlowJson": {
        "type": "object",
        "required": [
          "version",
          "screens"
        ],
        "description": "Meta's Flow JSON document. Validated by Genuka before it reaches Meta (structure, one entry screen, every path reaching a terminal screen, screen ids `^[A-Za-z0-9_]+$`, at most 10 MB). Versions 3.0 to 7.3 are understood; 6.2 is the best documented.",
        "properties": {
          "version": {
            "type": "string",
            "examples": [
              "6.2"
            ]
          },
          "data_api_version": {
            "type": "string",
            "description": "Endpoint-backed Flows only; must be `3.0`."
          },
          "routing_model": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Screen adjacency. Mandatory for an endpoint-backed Flow."
          },
          "screens": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": [
                "id",
                "layout"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^[A-Za-z0-9_]+$"
                },
                "title": {
                  "type": "string"
                },
                "terminal": {
                  "type": "boolean"
                },
                "success": {
                  "type": "boolean"
                },
                "refresh_on_back": {
                  "type": "boolean"
                },
                "data": {
                  "type": "object",
                  "additionalProperties": true
                },
                "sensitive": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "layout": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        },
        "additionalProperties": true
      },
      "FlowValidationError": {
        "type": "object",
        "description": "One of Meta's structural complaints about the Flow JSON, returned verbatim with its location.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "error_type": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "line_start": {
            "type": "integer"
          },
          "line_end": {
            "type": "integer"
          },
          "column_start": {
            "type": "integer"
          },
          "column_end": {
            "type": "integer"
          },
          "pointers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "line_start": {
                  "type": "integer"
                },
                "line_end": {
                  "type": "integer"
                },
                "path": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Flow": {
        "type": "object",
        "required": [
          "id",
          "companyId",
          "name",
          "categories",
          "status",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "flw_1"
            ]
          },
          "companyId": {
            "type": "string"
          },
          "connectionId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The number the Flow was created with; null if that number was removed."
          },
          "providerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta's Flow id: the `flow_id` to put in an interactive flow message."
          },
          "name": {
            "type": "string"
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlowCategory"
            }
          },
          "status": {
            "$ref": "#/components/schemas/FlowStatus"
          },
          "flowJson": {
            "description": "The last Flow JSON sent through this API. Not included in list responses.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/FlowJson"
              },
              {
                "type": "null"
              }
            ]
          },
          "version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Flow JSON version of `flowJson`."
          },
          "endpointUri": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FlowListResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Flow"
            }
          }
        }
      },
      "FlowResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Flow"
          },
          "validationErrors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlowValidationError"
            },
            "description": "Meta's validation errors, when Meta returned them (on create, on publish, and on a read that reached Meta). Empty array when none."
          }
        }
      },
      "FlowCreateRequest": {
        "type": "object",
        "required": [
          "connectionId",
          "name",
          "categories"
        ],
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Number whose WhatsApp Business Account will own the Flow.",
            "examples": [
              "con_1"
            ]
          },
          "name": {
            "type": "string",
            "description": "Unique per client."
          },
          "categories": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/FlowCategory"
            }
          },
          "flowJson": {
            "allOf": [
              {
                "$ref": "#/components/schemas/FlowJson"
              }
            ],
            "description": "Optional: a Flow can be created empty and filled later with PATCH."
          },
          "endpointUri": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS endpoint for a data-exchange Flow."
          },
          "cloneFlowId": {
            "type": "string",
            "description": "Meta Flow id to copy instead of starting empty."
          },
          "publish": {
            "type": "boolean",
            "description": "Publish immediately. Meta re-validates and may still refuse."
          }
        }
      },
      "FlowUpdateRequest": {
        "type": "object",
        "description": "At least one field. `flowJson` replaces the whole document.",
        "properties": {
          "name": {
            "type": "string"
          },
          "categories": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/FlowCategory"
            }
          },
          "endpointUri": {
            "type": "string",
            "format": "uri"
          },
          "flowJson": {
            "$ref": "#/components/schemas/FlowJson"
          },
          "status": {
            "const": "deprecated",
            "description": "The only status change accepted here, and only on a published Flow."
          }
        }
      },
      "FlowDeleteResponse": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true
          }
        }
      },
      "FlowPreviewResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "url",
              "expiresAt"
            ],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri",
                "description": "Meta's preview link, embeddable in an iframe. Valid 30 days."
              },
              "expiresAt": {
                "type": "string",
                "description": "Expiry as returned by Meta."
              }
            }
          }
        }
      },
      "CampaignStatus": {
        "type": "string",
        "enum": [
          "draft",
          "scheduled",
          "running",
          "completed",
          "failed"
        ],
        "description": "`draft` until launched, `running` during a launch, then `completed`, or `failed` when nothing was sent and at least one recipient failed. `scheduled` exists in the model but is never set by the API: there is no scheduling, a campaign goes out when you launch it."
      },
      "CampaignRecipientStatus": {
        "type": "string",
        "enum": [
          "pending",
          "sent",
          "delivered",
          "read",
          "failed",
          "skipped",
          "deleted"
        ],
        "description": "`pending`: not sent yet (also a recipient Meta throttled during a launch, picked up by the next launch). `sent`, `delivered`, `read`: progression reported by Meta's status webhooks. `failed`: refused by Meta at send time or reported failed later, see `errorMessage`. `skipped`: excluded because the recipient opted out of marketing (MARKETING templates only). `deleted`: the message was deleted."
      },
      "CampaignRecipientVariables": {
        "description": "How to fill the template for this recipient. Simple form: an array of positional body parameters (`{{1}}`, `{{2}}`, …). Rich form: an object with the same fields as `template` on `POST /api/v1/messages` (`body`, `bodyNamed`, `header`, `buttons`, `otp`, `components`), for media headers, dynamic buttons, named parameters or one-time codes. Any other type is ignored.",
        "oneOf": [
          {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                }
              ]
            },
            "examples": [
              [
                "Alice",
                "#1024"
              ]
            ]
          },
          {
            "type": "object",
            "properties": {
              "body": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MessageTemplateParamValue"
                }
              },
              "bodyNamed": {
                "type": "object",
                "additionalProperties": {
                  "$ref": "#/components/schemas/MessageTemplateParamValue"
                }
              },
              "header": {
                "$ref": "#/components/schemas/MessageTemplateHeader"
              },
              "buttons": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MessageTemplateButton"
                }
              },
              "otp": {
                "type": "string"
              },
              "components": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        ]
      },
      "CampaignRecipientInput": {
        "type": "object",
        "required": [
          "to"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "Phone number in international format with country code. Entries with an empty `to` are dropped.",
            "examples": [
              "+237690000001"
            ]
          },
          "variables": {
            "$ref": "#/components/schemas/CampaignRecipientVariables"
          }
        }
      },
      "CampaignCreateRequest": {
        "type": "object",
        "required": [
          "connectionId",
          "templateId",
          "name",
          "recipients"
        ],
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "The sending number.",
            "examples": [
              "con_1"
            ]
          },
          "templateId": {
            "type": "string",
            "description": "Genuka template id (`id` from `GET /api/v1/templates`), of the same client as the number. It need not be approved yet to create the campaign, but must be approved to launch it.",
            "examples": [
              "tpl_124"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "October promo"
            ]
          },
          "recipients": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/CampaignRecipientInput"
            }
          }
        }
      },
      "CampaignCreateResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "id",
              "name",
              "status",
              "_count"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "$ref": "#/components/schemas/CampaignStatus"
              },
              "_count": {
                "type": "object",
                "properties": {
                  "recipients": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "CampaignListItem": {
        "type": "object",
        "required": [
          "id",
          "companyId",
          "connectionId",
          "templateId",
          "name",
          "status",
          "createdAt",
          "_count"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "companyId": {
            "type": "string"
          },
          "connectionId": {
            "type": "string"
          },
          "templateId": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "_count": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              }
            }
          }
        }
      },
      "CampaignListResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CampaignListItem"
            }
          }
        }
      },
      "CampaignDetail": {
        "type": "object",
        "required": [
          "id",
          "companyId",
          "connectionId",
          "name",
          "status",
          "createdAt",
          "updatedAt",
          "_count",
          "stats"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "companyId": {
            "type": "string"
          },
          "connectionId": {
            "type": "string"
          },
          "templateId": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "messageType": {
            "type": "string",
            "description": "Always `template` for campaigns created through the API."
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "scheduledAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Not used by the API (always null for API campaigns)."
          },
          "costEstimate": {
            "type": "string",
            "description": "Legacy decimal field, serialized as a string; not set by the API (\"0\"). Genuka does not charge per message."
          },
          "metadata": {
            "description": "Not set by the API (null)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "company": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string"
              }
            }
          },
          "template": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "language": {
                    "type": "string"
                  },
                  "status": {
                    "$ref": "#/components/schemas/TemplateStatus"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "connection": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "displayPhoneNumber": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "phoneNumberId": {
                "type": "string"
              }
            }
          },
          "_count": {
            "type": "object",
            "properties": {
              "recipients": {
                "type": "integer"
              }
            }
          },
          "stats": {
            "type": "object",
            "description": "Number of recipients per status (only statuses that occur are present).",
            "additionalProperties": {
              "type": "integer"
            },
            "examples": [
              {
                "sent": 1180,
                "delivered": 1650,
                "read": 940,
                "failed": 22,
                "skipped": 8
              }
            ]
          }
        }
      },
      "CampaignDetailResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CampaignDetail"
          }
        }
      },
      "CampaignRecipient": {
        "type": "object",
        "required": [
          "id",
          "contact",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "contact": {
            "type": "string",
            "description": "The recipient's phone number, as given at creation."
          },
          "status": {
            "$ref": "#/components/schemas/CampaignRecipientStatus"
          },
          "providerMessageId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta's wamid once sent; the key of the status webhooks."
          },
          "costCredits": {
            "type": "integer",
            "description": "Legacy, always 0: Genuka does not charge per message."
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the recipient failed or was skipped."
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "readAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "failedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CampaignRecipientListResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CampaignRecipient"
            }
          }
        }
      },
      "CampaignLaunchResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "sent",
              "failed",
              "skipped"
            ],
            "properties": {
              "sent": {
                "type": "integer",
                "description": "Recipients Meta accepted in this launch."
              },
              "failed": {
                "type": "integer",
                "description": "Recipients Meta refused (see their `errorMessage`)."
              },
              "skipped": {
                "type": "integer",
                "description": "Recipients excluded because they opted out of marketing."
              }
            }
          }
        }
      },
      "NumberQualityRating": {
        "type": "string",
        "description": "Meta's quality rating of the number. `UNKNOWN` (or null before the first read) means Meta has too little traffic to score it; it is not a degradation.",
        "enum": [
          "GREEN",
          "YELLOW",
          "RED",
          "UNKNOWN"
        ]
      },
      "NumberMessagingLimitTier": {
        "type": "string",
        "description": "Meta's messaging limit tier, ascending: how many unique customers can receive business-initiated (template) messages in a rolling 24 hours. A business that has not passed Meta business verification stays at `TIER_250`.",
        "enum": [
          "TIER_50",
          "TIER_250",
          "TIER_1K",
          "TIER_10K",
          "TIER_100K",
          "TIER_UNLIMITED"
        ]
      },
      "NumberPlatformType": {
        "type": "string",
        "description": "`SMB_APP` means coexistence: the number is shared with the WhatsApp Business app (the app keeps working) and its throughput is fixed at 20 messages per second. `CLOUD_API` is a number used through the API only.",
        "enum": [
          "CLOUD_API",
          "SMB_APP",
          "ON_PREMISE",
          "NOT_APPLICABLE"
        ]
      },
      "NumberThroughputLevel": {
        "type": "string",
        "description": "Graph's `throughput.level`. `STANDARD` = 80 messages per second; `HIGH` and `HIGH_THROUGHPUT` both mean the 1,000 mps upgrade. Ignored for coexistence numbers.",
        "examples": [
          "STANDARD"
        ]
      },
      "NumberThroughputLimits": {
        "type": "object",
        "description": "Send rates, in messages per second, used to derive `messagesPerSecond`.",
        "properties": {
          "default": {
            "type": "integer",
            "examples": [
              80
            ]
          },
          "upgraded": {
            "type": "integer",
            "examples": [
              1000
            ]
          },
          "coexistence": {
            "type": "integer",
            "examples": [
              20
            ]
          },
          "upgradedLevels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "examples": [
              [
                "HIGH",
                "HIGH_THROUGHPUT"
              ]
            ]
          }
        },
        "required": [
          "default",
          "upgraded",
          "coexistence",
          "upgradedLevels"
        ]
      },
      "NumberHealthAlert": {
        "type": "object",
        "description": "Something that changed between the previously stored health and the read just made. Never emitted on the first read of a number.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "quality_dropped",
              "quality_recovered",
              "messaging_limit_changed",
              "throughput_changed",
              "status_changed",
              "sandbox_mode"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "critical"
            ]
          },
          "from": {
            "type": [
              "string",
              "null"
            ],
            "description": "Previous value, null when unknown."
          },
          "to": {
            "type": "string",
            "description": "New value."
          },
          "message": {
            "type": "string",
            "description": "Explanation written for a human on call."
          }
        },
        "required": [
          "kind",
          "severity",
          "from",
          "to",
          "message"
        ]
      },
      "NumberSummary": {
        "type": "object",
        "description": "A connected WhatsApp number with its health. Fields after `messagesPerSecond` are present only when the list was requested with `refresh=true`.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Genuka connection id: the `connectionId` every other endpoint takes."
          },
          "companyId": {
            "type": "string",
            "description": "The client (company) this number belongs to."
          },
          "wabaId": {
            "type": "string",
            "description": "Meta WhatsApp Business Account id."
          },
          "phoneNumberId": {
            "type": "string",
            "description": "Meta phone number id."
          },
          "displayPhoneNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "verifiedName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name approved by Meta."
          },
          "qualityRating": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last stored quality rating: `GREEN`, `YELLOW`, `RED` or `UNKNOWN` once a health read has run, null before. Read with `refresh=true` or `GET /api/v1/numbers/{id}/health` when the value matters."
          },
          "messagingLimitTier": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/NumberMessagingLimitTier"
              },
              {
                "type": "null"
              }
            ]
          },
          "throughputLevel": {
            "type": [
              "string",
              "null"
            ],
            "description": "See NumberThroughputLevel."
          },
          "platformType": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/NumberPlatformType"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "description": "Genuka lifecycle status of the connection (`connected`, `disconnected`, `flagged`, `offboarded`). Not Meta's number status.",
            "examples": [
              "connected"
            ]
          },
          "isMain": {
            "type": "boolean",
            "description": "The client's main number: the default for client-scoped calls that omit `connectionId`."
          },
          "offboardedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set when Meta reported the number offboarded."
          },
          "releasedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set when the partner released the number to free its plan slot. A released number cannot send (`409 number_released`) until it is reconnected through Embedded Signup."
          },
          "coexistence": {
            "type": "boolean",
            "description": "`platformType === \"SMB_APP\"`."
          },
          "messagesPerSecond": {
            "type": "integer",
            "description": "Derived send rate: 20 for coexistence, 1000 when upgraded, otherwise 80."
          },
          "metaStatus": {
            "type": [
              "string",
              "null"
            ],
            "description": "refresh=true only: Meta's own number status (`CONNECTED`, `FLAGGED`, `RESTRICTED`, …)."
          },
          "refreshed": {
            "type": "boolean",
            "description": "refresh=true only: whether Meta was read for this number."
          },
          "alerts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NumberHealthAlert"
            },
            "description": "refresh=true only, when `refreshed` is true."
          },
          "refreshError": {
            "type": "string",
            "description": "refresh=true only, when `refreshed` is false: `account_deactivated` for a suspended client, otherwise Meta's error class or message. The stored values are returned instead."
          }
        },
        "required": [
          "id",
          "companyId",
          "wabaId",
          "phoneNumberId",
          "displayPhoneNumber",
          "verifiedName",
          "qualityRating",
          "messagingLimitTier",
          "throughputLevel",
          "platformType",
          "status",
          "isMain",
          "offboardedAt",
          "releasedAt",
          "coexistence",
          "messagesPerSecond"
        ]
      },
      "NumberListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NumberSummary"
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "database",
              "meta"
            ],
            "description": "`database` by default, `meta` when `refresh=true`."
          },
          "limits": {
            "type": "object",
            "description": "refresh=true only.",
            "properties": {
              "throughput": {
                "$ref": "#/components/schemas/NumberThroughputLimits"
              },
              "refreshMax": {
                "type": "integer",
                "description": "Most numbers a refresh may cover.",
                "examples": [
                  25
                ]
              }
            },
            "required": [
              "throughput",
              "refreshMax"
            ]
          }
        },
        "required": [
          "data",
          "source"
        ]
      },
      "NumberHealth": {
        "type": "object",
        "description": "Live health of one number, as just read from Meta.",
        "properties": {
          "connectionId": {
            "type": "string"
          },
          "companyId": {
            "type": "string"
          },
          "wabaId": {
            "type": "string"
          },
          "phoneNumberId": {
            "type": "string"
          },
          "displayPhoneNumber": {
            "type": "string"
          },
          "verifiedName": {
            "type": "string"
          },
          "qualityRating": {
            "$ref": "#/components/schemas/NumberQualityRating"
          },
          "messagingLimitTier": {
            "$ref": "#/components/schemas/NumberMessagingLimitTier"
          },
          "throughputLevel": {
            "$ref": "#/components/schemas/NumberThroughputLevel"
          },
          "platformType": {
            "$ref": "#/components/schemas/NumberPlatformType"
          },
          "status": {
            "type": "string",
            "description": "Meta's number status, as Meta returned it (same value as `metaStatus`).",
            "enum": [
              "PENDING",
              "CONNECTED",
              "FLAGGED",
              "RESTRICTED",
              "RATE_LIMITED",
              "BANNED",
              "MIGRATED",
              "DELETED",
              "UNVERIFIED"
            ]
          },
          "codeVerificationStatus": {
            "type": "string",
            "description": "Meta's `code_verification_status`."
          },
          "accountMode": {
            "type": "string",
            "description": "`LIVE` or `SANDBOX`; absent means live.",
            "examples": [
              "LIVE"
            ]
          },
          "sandbox": {
            "type": "boolean",
            "description": "True in SANDBOX mode: the number only delivers to recipients registered on it, so a campaign fails for everyone else."
          },
          "coexistence": {
            "type": "boolean",
            "description": "Shared with the WhatsApp Business app (`platformType: SMB_APP`)."
          },
          "messagesPerSecond": {
            "type": "integer"
          },
          "metaStatus": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta's own number status."
          },
          "genukaStatus": {
            "type": "string",
            "description": "Genuka lifecycle status of the connection.",
            "examples": [
              "connected"
            ]
          },
          "offboardedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "connectionId",
          "companyId",
          "wabaId",
          "phoneNumberId",
          "qualityRating",
          "sandbox",
          "coexistence",
          "messagesPerSecond",
          "metaStatus",
          "genukaStatus",
          "offboardedAt"
        ]
      },
      "NumberHealthResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/NumberHealth"
          },
          "alerts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NumberHealthAlert"
            },
            "description": "What changed since the previous stored read. Empty on the first read and when nothing moved."
          },
          "limits": {
            "type": "object",
            "properties": {
              "throughput": {
                "$ref": "#/components/schemas/NumberThroughputLimits"
              },
              "tiers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/NumberMessagingLimitTier"
                },
                "description": "Every messaging limit tier, ascending."
              }
            },
            "required": [
              "throughput",
              "tiers"
            ]
          }
        },
        "required": [
          "data",
          "alerts",
          "limits"
        ]
      },
      "ConnectionSummary": {
        "type": "object",
        "description": "One WhatsApp connection: a WhatsApp Business Account plus one phone number, owned by one client.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The `connectionId` to pass when sending, creating templates or campaigns."
          },
          "companyId": {
            "type": "string"
          },
          "wabaId": {
            "type": "string"
          },
          "wabaName": {
            "type": [
              "string",
              "null"
            ]
          },
          "phoneNumberId": {
            "type": "string"
          },
          "displayPhoneNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "verifiedName": {
            "type": [
              "string",
              "null"
            ]
          },
          "qualityRating": {
            "type": [
              "string",
              "null"
            ],
            "description": "GREEN, YELLOW, RED or UNKNOWN; null before the first health read."
          },
          "status": {
            "type": "string",
            "description": "Genuka lifecycle status (`connected`, `disconnected`, `flagged`, `offboarded`)."
          },
          "isMain": {
            "type": "boolean"
          },
          "companyName": {
            "type": "string",
            "description": "Name of the client that owns the number."
          },
          "externalRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own identifier for that client, as passed in `/connect/<slug>?ref=`."
          }
        },
        "required": [
          "id",
          "companyId",
          "wabaId",
          "wabaName",
          "phoneNumberId",
          "displayPhoneNumber",
          "verifiedName",
          "qualityRating",
          "status",
          "isMain",
          "companyName",
          "externalRef"
        ]
      },
      "ConnectionListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConnectionSummary"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookEvent": {
        "type": "string",
        "description": "Public Genuka event names an endpoint can subscribe to. On input, the legacy Meta field names (`messages`, `message_template_status_update`, `message_template_quality_update`, `template_category_update`, `message_template_components_update`, `user_preferences`, `account_update`, `account_alerts`, `account_review_update`, `business_capability_update`, `security`, `phone_number_quality_update`, `phone_number_name_update`, `flows`, `history`, `smb_app_state_sync`, `smb_message_echoes`, `account_offboarded`, `account_reconnected`) are also accepted and expanded to every Genuka event they carry. Matching happens on the underlying Meta field: subscribing to any `message.*` event (or `flow.response_received`) delivers every event of Meta's `messages` field (inbound messages and all delivery statuses), so filter on the envelope `type` in your receiver. `coexistence.*` names are accepted but coexistence events are not currently forwarded to endpoints.",
        "enum": [
          "message.received",
          "message.sent",
          "message.delivered",
          "message.read",
          "message.failed",
          "message.deleted",
          "template.status_changed",
          "template.quality_changed",
          "template.category_changed",
          "template.components_changed",
          "user_preference.stopped",
          "user_preference.resumed",
          "account.alert",
          "account.updated",
          "account.review_completed",
          "account.capability_changed",
          "account.security_changed",
          "number.quality_changed",
          "number.name_changed",
          "flow.response_received",
          "flow.status_changed",
          "coexistence.history_received",
          "coexistence.contacts_synced",
          "coexistence.message_echoed",
          "coexistence.offboarded",
          "coexistence.reconnected",
          "unknown.received"
        ]
      },
      "WebhookDeliveryCounts": {
        "type": "object",
        "description": "Deliveries currently in the log for this endpoint, by status.",
        "properties": {
          "success": {
            "type": "integer"
          },
          "pending": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          }
        },
        "required": [
          "success",
          "pending",
          "failed"
        ]
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Label; defaults to the URL's host."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEvent"
            },
            "description": "Subscribed events, as Genuka names. An empty array means every event."
          },
          "enabled": {
            "type": "boolean",
            "description": "False pauses deliveries: nothing new is queued for the endpoint."
          },
          "scope": {
            "type": "string",
            "enum": [
              "partner",
              "company",
              "connection"
            ],
            "description": "`partner`: every number on the account; `company`: every number of one client; `connection`: one number."
          },
          "companyId": {
            "type": [
              "string",
              "null"
            ]
          },
          "connectionId": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "deliveries": {
            "$ref": "#/components/schemas/WebhookDeliveryCounts"
          }
        },
        "required": [
          "id",
          "name",
          "url",
          "events",
          "enabled",
          "scope",
          "companyId",
          "connectionId",
          "createdAt",
          "updatedAt",
          "deliveries"
        ]
      },
      "WebhookEndpointWithSecret": {
        "description": "An endpoint plus its signing secret. Returned only at creation and on `rotateSecret`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEndpoint"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "HMAC-SHA256 signing secret (`whsec_…`). Shown once: store it now, it is never returned by a GET.",
                "examples": [
                  "whsec_3q2-xYcK8vN1mZ0pL7tR5wE9uI4oA6sD2fG8hJ1kL0"
                ]
              }
            },
            "required": [
              "secret"
            ]
          }
        ]
      },
      "WebhookCreateRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public https URL. No `user:password@`, and the host must resolve to a public address (private, loopback and link-local hosts are refused)."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Genuka event names (see WebhookEvent) or legacy Meta field names. Omitted, empty or the full list = every event. Unknown names are rejected."
          },
          "enabled": {
            "type": "boolean",
            "default": true
          },
          "name": {
            "type": "string",
            "description": "Label. Defaults to the URL's host."
          },
          "companyId": {
            "type": "string",
            "description": "Cover every number of this client. Defaults to the key's own client for a client-scoped key."
          },
          "connectionId": {
            "type": "string",
            "description": "Cover one number only. Takes precedence over `companyId`."
          }
        },
        "required": [
          "url"
        ]
      },
      "WebhookUpdateRequest": {
        "type": "object",
        "description": "Partial update: absent fields are left unchanged. The endpoint's scope cannot be changed.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Same rules as at creation."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Replaces the subscription. `[]` means every event."
          },
          "enabled": {
            "type": "boolean"
          },
          "name": {
            "type": "string",
            "description": "Must not be blank."
          },
          "rotateSecret": {
            "type": "boolean",
            "description": "true generates a new signing secret, returned in this response only. The previous secret stops verifying immediately."
          }
        }
      },
      "WebhookResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/WebhookEndpoint"
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookWithSecretResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/WebhookEndpointWithSecret"
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookUpdateResponse": {
        "type": "object",
        "properties": {
          "data": {
            "description": "The updated endpoint. Carries `secret` only when `rotateSecret: true` was sent.",
            "allOf": [
              {
                "$ref": "#/components/schemas/WebhookEndpoint"
              },
              {
                "type": "object",
                "properties": {
                  "secret": {
                    "type": "string",
                    "description": "Present only after a rotation."
                  }
                }
              }
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookDeleteResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "deleted": {
            "type": "string",
            "description": "Id of the deleted endpoint."
          }
        },
        "required": [
          "ok",
          "deleted"
        ]
      },
      "WebhookTestResult": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "description": "Envelope `type` of the synthetic event."
          },
          "field": {
            "type": "string",
            "description": "Meta field of the synthetic event."
          },
          "status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status your endpoint answered; null when the request never completed."
          },
          "latencyMs": {
            "type": "integer"
          },
          "ok": {
            "type": "boolean",
            "description": "True on any 2xx."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "`http_<status>`, `timeout`, or a network error message; null on success."
          }
        },
        "required": [
          "event",
          "field",
          "status",
          "latencyMs",
          "ok",
          "error"
        ]
      },
      "WebhookTestResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string"
              },
              "sent": {
                "type": "integer"
              },
              "succeeded": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "results": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WebhookTestResult"
                }
              }
            },
            "required": [
              "url",
              "sent",
              "succeeded",
              "failed",
              "results"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "WebhookEventEnvelope": {
        "type": "object",
        "description": "The JSON body POSTed to an endpoint (one event per request). On the wire it also carries `id` (the delivery id, also in `X-Genuka-Delivery`) as its first key; the stored `payload` of a delivery omits it. For the four original Meta fields (`messages`, `message_template_status_update`, `account_update`, `phone_number_quality_update`) `type` is a legacy name (`inbound_message`, `message_status`, `template_status`, `account_update`, `phone_quality`) and `data` is Meta's raw object; for every other field `type` is a Genuka event name and `data` is the normalized Genuka payload. Ignore fields you do not know: payloads may gain fields.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Delivery id (wire body only)."
          },
          "type": {
            "type": "string",
            "examples": [
              "message_status"
            ]
          },
          "field": {
            "type": "string",
            "description": "Meta webhook field.",
            "examples": [
              "messages"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "partner_id": {
            "type": "string"
          },
          "company_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "connection_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "waba_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone_number_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "type",
          "field",
          "created_at",
          "partner_id",
          "data"
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Delivery id: the `id` of the body sent and the `X-Genuka-Delivery` header."
          },
          "event": {
            "type": "string",
            "description": "Envelope `type`.",
            "examples": [
              "message_status"
            ]
          },
          "field": {
            "type": "string",
            "description": "Meta webhook field.",
            "examples": [
              "messages"
            ]
          },
          "eventKey": {
            "type": "string",
            "description": "Idempotency key: an event is delivered at most once per endpoint."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "failed"
            ],
            "description": "`pending`: queued or waiting for a retry. `failed`: retries exhausted, or the endpoint was disabled."
          },
          "attempts": {
            "type": "integer"
          },
          "maxAttempts": {
            "type": "integer",
            "description": "Attempts before giving up.",
            "examples": [
              6
            ]
          },
          "responseStatus": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Last HTTP status your endpoint answered."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last failure: `http_<status>`, `timeout`, `endpoint_disabled` or a network error."
          },
          "nextAttemptAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Next retry; null once settled."
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "payload": {
            "$ref": "#/components/schemas/WebhookEventEnvelope"
          }
        },
        "required": [
          "id",
          "event",
          "field",
          "eventKey",
          "status",
          "attempts",
          "maxAttempts",
          "responseStatus",
          "error",
          "nextAttemptAt",
          "deliveredAt",
          "createdAt",
          "updatedAt",
          "payload"
        ]
      },
      "WebhookDeliveryListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Pass as `cursor` for the next page; null on the last page."
              },
              "retentionDays": {
                "type": "integer",
                "description": "How long deliveries are kept on the account's plan."
              }
            },
            "required": [
              "nextCursor",
              "retentionDays"
            ]
          }
        },
        "required": [
          "data",
          "meta"
        ]
      },
      "WebhookReplayResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "const": "pending"
              },
              "queued": {
                "type": "boolean",
                "const": true
              }
            },
            "required": [
              "id",
              "status",
              "queued"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "CompanyConnectionSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Connection id (`connectionId`)."
          },
          "wabaId": {
            "type": "string"
          },
          "phoneNumberId": {
            "type": "string"
          },
          "displayPhoneNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "verifiedName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Only in `GET /api/v1/companies/{id}`."
          },
          "qualityRating": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "isMain": {
            "type": "boolean",
            "description": "Only in `GET /api/v1/companies/{id}`."
          }
        },
        "required": [
          "id",
          "wabaId",
          "phoneNumberId",
          "displayPhoneNumber",
          "qualityRating",
          "status"
        ]
      },
      "CompanySummary": {
        "type": "object",
        "description": "A client: a business whose WhatsApp numbers are connected to the account.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "externalRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your identifier for this client, from `/connect/<slug>?ref=`."
          },
          "onboardedAt": {
            "type": "string",
            "format": "date-time"
          },
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompanyConnectionSummary"
            }
          },
          "_count": {
            "type": "object",
            "properties": {
              "templates": {
                "type": "integer"
              }
            },
            "required": [
              "templates"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "externalRef",
          "onboardedAt",
          "connections",
          "_count"
        ]
      },
      "CompanyListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompanySummary"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "CompanyDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "partnerId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended"
            ]
          },
          "deactivatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "currency": {
            "type": "string",
            "examples": [
              "XAF"
            ]
          },
          "externalRef": {
            "type": [
              "string",
              "null"
            ]
          },
          "apiAccessEnabled": {
            "type": "boolean",
            "description": "Whether the partner lets this client create its own API key from the client portal."
          },
          "onboardedAt": {
            "type": "string",
            "format": "date-time"
          },
          "connections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompanyConnectionSummary"
            }
          },
          "_count": {
            "type": "object",
            "properties": {
              "templates": {
                "type": "integer"
              },
              "campaigns": {
                "type": "integer"
              }
            },
            "required": [
              "templates",
              "campaigns"
            ]
          }
        },
        "required": [
          "id",
          "partnerId",
          "name",
          "status",
          "deactivatedAt",
          "currency",
          "externalRef",
          "apiAccessEnabled",
          "onboardedAt",
          "connections",
          "_count"
        ]
      },
      "CompanyResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CompanyDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "ProfileBusinessVertical": {
        "type": "string",
        "description": "Business category. Closed list: Meta answers an undocumented 400 for anything else.",
        "enum": [
          "UNDEFINED",
          "OTHER",
          "AUTO",
          "BEAUTY",
          "APPAREL",
          "EDU",
          "ENTERTAIN",
          "EVENT_PLAN",
          "FINANCE",
          "GROCERY",
          "GOVT",
          "HOTEL",
          "HEALTH",
          "NONPROFIT",
          "PROF_SERVICES",
          "RETAIL",
          "TRAVEL",
          "RESTAURANT",
          "NOT_A_BIZ"
        ]
      },
      "ProfileBusinessProfile": {
        "type": "object",
        "description": "The WhatsApp Business profile as Meta holds it. Every field is optional: a fresh number has an empty profile.",
        "properties": {
          "about": {
            "type": "string",
            "description": "Line under the business name in the chat header."
          },
          "address": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "websites": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "vertical": {
            "type": "string",
            "description": "One of ProfileBusinessVertical (kept as-is if Meta returns a newer value)."
          },
          "profilePictureUrl": {
            "type": "string",
            "description": "CDN URL of the picture. Read-only."
          }
        }
      },
      "ProfileUpdateRequest": {
        "type": "object",
        "description": "Partial update: only the fields present are sent to Meta. Send at least one profile field.",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Required with a partner-wide key."
          },
          "about": {
            "type": "string",
            "maxLength": 139
          },
          "address": {
            "type": "string",
            "maxLength": 256
          },
          "description": {
            "type": "string",
            "maxLength": 512
          },
          "email": {
            "type": "string",
            "maxLength": 128,
            "description": "Must look like an email address."
          },
          "websites": {
            "type": "array",
            "maxItems": 2,
            "items": {
              "type": "string",
              "maxLength": 256,
              "description": "Absolute http(s) URL, scheme included."
            }
          },
          "vertical": {
            "$ref": "#/components/schemas/ProfileBusinessVertical"
          },
          "profilePictureHandle": {
            "type": "string",
            "description": "Handle from Meta's Resumable Upload API (`h=…`, e.g. `4::…`). Not a media id and not a URL."
          }
        }
      },
      "ProfileResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "connectionId": {
                "type": "string"
              },
              "phoneNumberId": {
                "type": "string"
              },
              "profile": {
                "$ref": "#/components/schemas/ProfileBusinessProfile"
              }
            },
            "required": [
              "connectionId",
              "phoneNumberId",
              "profile"
            ]
          }
        },
        "required": [
          "data"
        ]
      },
      "SubscriptionResponse": {
        "type": "object",
        "description": "Not wrapped in `data`.",
        "properties": {
          "plan": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "examples": [
                  "growth"
                ],
                "description": "`starter`, `growth`, `scale`, `enterprise`, or a custom plan code."
              },
              "name": {
                "type": "string",
                "examples": [
                  "Growth"
                ]
              }
            },
            "required": [
              "code",
              "name"
            ]
          },
          "interval": {
            "type": "string",
            "enum": [
              "monthly",
              "annual"
            ]
          },
          "currency": {
            "type": "string",
            "examples": [
              "XAF"
            ],
            "description": "XAF, XOF, EUR or USD."
          },
          "status": {
            "type": "string",
            "enum": [
              "trialing",
              "active",
              "past_due",
              "canceled"
            ],
            "description": "`past_due`: the trial or the paid period lapsed without renewal; sends are refused with `402 subscription_past_due`."
          },
          "currentPeriodEnd": {
            "type": "string",
            "format": "date-time"
          },
          "billedNumbers": {
            "type": "integer",
            "description": "WhatsApp numbers paid for this period: the billing unit."
          },
          "usage": {
            "type": "object",
            "properties": {
              "numbers": {
                "type": "integer",
                "description": "Connected numbers occupying a slot (released numbers excluded)."
              },
              "messages": {
                "type": "integer",
                "description": "Messages counted against this period's allowance."
              },
              "seats": {
                "type": "integer",
                "description": "Dashboard users."
              }
            },
            "required": [
              "numbers",
              "messages",
              "seats"
            ]
          },
          "limits": {
            "type": "object",
            "description": "Effective limits for the current period. null = unlimited.",
            "properties": {
              "maxNumbers": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "monthlyMessages": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Per-number quota × numbers (billed + referral bonus)."
              },
              "maxSeats": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "maxNumbers",
              "monthlyMessages",
              "maxSeats"
            ]
          }
        },
        "required": [
          "plan",
          "interval",
          "currency",
          "status",
          "currentPeriodEnd",
          "billedNumbers",
          "usage",
          "limits"
        ]
      },
      "Invoice": {
        "type": "object",
        "description": "Amounts are integers in minor units of `currency`: XAF and XOF have none (15000 = 15,000 XAF), EUR and USD are in cents.",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "string",
            "examples": [
              "INV-2026-0042"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "paid",
              "void"
            ],
            "description": "Stored status."
          },
          "kind": {
            "type": "string",
            "enum": [
              "subscription",
              "proration"
            ],
            "description": "`subscription`: a whole period. `proration`: the days left in a period after a mid-period change."
          },
          "state": {
            "type": "string",
            "enum": [
              "paid",
              "due",
              "overdue",
              "void"
            ],
            "description": "Derived: an `open` invoice is `overdue` once past `dueAt`, otherwise `due`."
          },
          "plan": {
            "type": "object",
            "description": "Snapshot at issue time.",
            "properties": {
              "code": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "name"
            ]
          },
          "interval": {
            "type": "string",
            "enum": [
              "monthly",
              "annual"
            ]
          },
          "currency": {
            "type": "string"
          },
          "numbers": {
            "type": "integer",
            "description": "WhatsApp numbers billed."
          },
          "amountDue": {
            "type": "integer"
          },
          "referralCreditAmount": {
            "type": "integer",
            "description": "Referral credit deducted."
          },
          "welcomeDiscountAmount": {
            "type": "integer"
          },
          "periodStart": {
            "type": "string",
            "format": "date-time"
          },
          "periodEnd": {
            "type": "string",
            "format": "date-time"
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time"
          },
          "dueAt": {
            "type": "string",
            "format": "date-time"
          },
          "paidAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "number",
          "status",
          "kind",
          "state",
          "plan",
          "interval",
          "currency",
          "numbers",
          "amountDue",
          "referralCreditAmount",
          "welcomeDiscountAmount",
          "periodStart",
          "periodEnd",
          "issuedAt",
          "dueAt",
          "paidAt"
        ]
      },
      "InvoicePayment": {
        "type": "object",
        "description": "One payment attempt against the invoice. `amount`/`currency` are the invoice's.",
        "properties": {
          "id": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "genuka_pay",
              "pawapay",
              "stripe",
              "referral_credit",
              "manual"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "failed"
            ]
          },
          "amount": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "provider",
          "status",
          "amount",
          "currency",
          "createdAt"
        ]
      },
      "InvoiceDetail": {
        "description": "Not wrapped in `data`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Invoice"
          },
          {
            "type": "object",
            "properties": {
              "payments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InvoicePayment"
                },
                "description": "Newest first."
              }
            },
            "required": [
              "payments"
            ]
          }
        ]
      },
      "InvoiceListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "AnalyticsCost": {
        "description": "Cost is either available with an amount, or unavailable with a reason. Never treat an unavailable cost as zero: Meta omits cost for messaging analytics by construction and for WhatsApp Business Accounts billed through a partner credit line.",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "available": {
                "const": true
              },
              "amount": {
                "type": "number"
              }
            },
            "required": [
              "available",
              "amount"
            ]
          },
          {
            "type": "object",
            "properties": {
              "available": {
                "const": false
              },
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "available",
              "reason"
            ]
          }
        ]
      },
      "AnalyticsPoint": {
        "type": "object",
        "properties": {
          "start": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "end": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "day": {
            "type": "string",
            "format": "date",
            "description": "UTC day of `start`."
          },
          "sent": {
            "type": "integer",
            "description": "kind=messaging."
          },
          "delivered": {
            "type": "integer",
            "description": "kind=messaging."
          },
          "conversation": {
            "type": "integer",
            "description": "kind=conversation."
          },
          "volume": {
            "type": "integer",
            "description": "kind=pricing."
          },
          "cost": {
            "$ref": "#/components/schemas/AnalyticsCost"
          },
          "dimensions": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Whatever Meta broke the point down by (country, phone number, pricing category, …)."
          }
        },
        "required": [
          "start",
          "end",
          "day",
          "cost",
          "dimensions"
        ]
      },
      "AnalyticsDay": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "totals": {
            "type": "object",
            "description": "Sum of the day's points. Cost is summed only when every point reported one.",
            "properties": {
              "sent": {
                "type": "integer"
              },
              "delivered": {
                "type": "integer"
              },
              "conversation": {
                "type": "integer"
              },
              "volume": {
                "type": "integer"
              },
              "cost": {
                "$ref": "#/components/schemas/AnalyticsCost"
              }
            },
            "required": [
              "cost"
            ]
          },
          "points": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsPoint"
            }
          }
        },
        "required": [
          "day",
          "totals",
          "points"
        ]
      },
      "AnalyticsKind": {
        "type": "string",
        "enum": [
          "messaging",
          "conversation",
          "pricing"
        ],
        "description": "`messaging`: messages sent and delivered (never carries cost). `conversation`: conversation counts and cost. `pricing`: message volume and cost."
      },
      "AnalyticsWindow": {
        "type": "object",
        "description": "The window actually covered, ISO 8601.",
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "start",
          "end"
        ]
      },
      "AnalyticsResponse": {
        "type": "object",
        "description": "Response when reading Meta (the default).",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "kind": {
                "$ref": "#/components/schemas/AnalyticsKind"
              },
              "source": {
                "type": "string",
                "const": "meta"
              },
              "granularity": {
                "type": "string"
              },
              "points": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AnalyticsPoint"
                }
              },
              "days": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AnalyticsDay"
                },
                "description": "Points rolled up per UTC day."
              }
            },
            "required": [
              "kind",
              "source",
              "granularity",
              "points",
              "days"
            ]
          },
          "window": {
            "$ref": "#/components/schemas/AnalyticsWindow"
          },
          "truncated": {
            "type": "boolean",
            "description": "True when `start` was older than Meta's 365-day lookback and was moved forward; read the tail with `source=archive`."
          },
          "archivedDays": {
            "type": "integer",
            "description": "Days written to the Genuka archive by this read."
          },
          "costAvailable": {
            "type": "boolean",
            "description": "False when no day in the response carries a cost."
          },
          "costNotice": {
            "type": "string",
            "description": "Why cost is unavailable, when `costAvailable` is false."
          },
          "notice": {
            "type": "string",
            "description": "Set when the window was truncated."
          },
          "retentionDays": {
            "type": "integer",
            "description": "Meta's lookback, in days.",
            "examples": [
              365
            ]
          }
        },
        "required": [
          "data",
          "window",
          "truncated",
          "archivedDays",
          "costAvailable",
          "retentionDays"
        ]
      },
      "AnalyticsArchiveDay": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "kind": {
            "$ref": "#/components/schemas/AnalyticsKind"
          },
          "metrics": {
            "type": "object",
            "description": "What was archived for that day.",
            "properties": {
              "sourceGranularity": {
                "type": "string",
                "description": "Granularity the day was originally read at."
              },
              "totals": {
                "type": "object",
                "additionalProperties": true
              },
              "points": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AnalyticsPoint"
                }
              }
            }
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "day",
          "kind",
          "metrics",
          "updatedAt"
        ]
      },
      "AnalyticsArchiveResponse": {
        "type": "object",
        "description": "Response with `source=archive`.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "kind": {
                "$ref": "#/components/schemas/AnalyticsKind"
              },
              "source": {
                "type": "string",
                "const": "archive"
              },
              "days": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AnalyticsArchiveDay"
                }
              }
            },
            "required": [
              "kind",
              "source",
              "days"
            ]
          },
          "window": {
            "$ref": "#/components/schemas/AnalyticsWindow"
          }
        },
        "required": [
          "data",
          "window"
        ]
      },
      "BlockedUsersMutationRequest": {
        "type": "object",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Required with a partner-wide key."
          },
          "numbers": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "items": {
              "type": "string",
              "description": "Phone number with country code. Spaces, dashes, dots, parentheses and a leading `+` are stripped; 6 to 20 digits remain."
            },
            "description": "Up to 1,000 numbers per request; duplicates are merged. May instead be sent as `?numbers=` (comma-separated)."
          }
        }
      },
      "BlockedUsersLimits": {
        "type": "object",
        "properties": {
          "perRequestMax": {
            "type": "integer",
            "examples": [
              1000
            ]
          },
          "totalMax": {
            "type": "integer",
            "examples": [
              64000
            ],
            "description": "Cap on the whole blocklist of a number."
          }
        },
        "required": [
          "perRequestMax",
          "totalMax"
        ]
      },
      "BlockedUsersOutcomeResponse": {
        "type": "object",
        "description": "Itemised outcome of a block or unblock. Every requested number appears in exactly one of `succeeded` or `failed`.",
        "properties": {
          "error": {
            "type": "string",
            "const": "block_failed",
            "description": "Present only on 422 (no number went through)."
          },
          "data": {
            "type": "object",
            "properties": {
              "connectionId": {
                "type": "string"
              },
              "action": {
                "type": "string",
                "enum": [
                  "block",
                  "unblock"
                ]
              },
              "requested": {
                "type": "integer",
                "description": "Numbers sent to Meta after normalization and de-duplication."
              },
              "succeeded": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "number": {
                      "type": "string",
                      "description": "The number as sent to Meta (digits only)."
                    },
                    "waId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "number"
                  ]
                }
              },
              "failed": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "number": {
                      "type": "string"
                    },
                    "code": {
                      "type": "integer",
                      "description": "Meta error code, e.g. 131047 (no message from this user in the last 24 hours)."
                    },
                    "message": {
                      "type": "string",
                      "description": "Meta's wording, verbatim."
                    },
                    "hint": {
                      "type": "string",
                      "description": "What to do about it, when known."
                    }
                  },
                  "required": [
                    "number",
                    "message"
                  ]
                }
              }
            },
            "required": [
              "connectionId",
              "action",
              "requested",
              "succeeded",
              "failed"
            ]
          },
          "limits": {
            "$ref": "#/components/schemas/BlockedUsersLimits"
          }
        },
        "required": [
          "data",
          "limits"
        ]
      },
      "BlockedUsersListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "waId": {
                  "type": "string"
                }
              },
              "required": [
                "waId"
              ]
            }
          },
          "paging": {
            "type": "object",
            "description": "Present only when there is a next page.",
            "properties": {
              "after": {
                "type": "string",
                "description": "Pass as `after` to read the next page."
              }
            },
            "required": [
              "after"
            ]
          },
          "connectionId": {
            "type": "string"
          }
        },
        "required": [
          "data",
          "connectionId"
        ]
      },
      "ConversationSettingsCommand": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 32,
            "description": "Without the leading slash (WhatsApp adds it) and without spaces. Unique, case-insensitive."
          },
          "description": {
            "type": "string",
            "maxLength": 256,
            "description": "Required: the hint shown next to the command."
          }
        },
        "required": [
          "name",
          "description"
        ]
      },
      "ConversationSettingsLimits": {
        "type": "object",
        "properties": {
          "iceBreakersMax": {
            "type": "integer",
            "examples": [
              4
            ]
          },
          "iceBreakerMax": {
            "type": "integer",
            "examples": [
              80
            ],
            "description": "Characters per ice breaker."
          },
          "commandsMax": {
            "type": "integer",
            "examples": [
              30
            ]
          },
          "commandNameMax": {
            "type": "integer",
            "examples": [
              32
            ]
          },
          "commandDescriptionMax": {
            "type": "integer",
            "examples": [
              256
            ]
          }
        },
        "required": [
          "iceBreakersMax",
          "iceBreakerMax",
          "commandsMax",
          "commandNameMax",
          "commandDescriptionMax"
        ]
      },
      "ConversationSettingsUpdateRequest": {
        "type": "object",
        "description": "Send at least one of `welcomeMessageEnabled`, `iceBreakers`, `commands`. A field you send replaces that whole list; fields you omit are kept.",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Required with a partner-wide key."
          },
          "welcomeMessageEnabled": {
            "type": "boolean",
            "description": "Sends nothing by itself: WhatsApp then emits a `request_welcome` inbound message when a customer opens a brand-new chat, and your integration replies to it."
          },
          "iceBreakers": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "string",
              "maxLength": 80
            },
            "description": "Tappable suggestions shown only in a chat with no history. Single line, non-empty, no duplicates."
          },
          "commands": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "$ref": "#/components/schemas/ConversationSettingsCommand"
            }
          }
        }
      },
      "ConversationSettingsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "connectionId": {
                "type": "string"
              },
              "phoneNumberId": {
                "type": "string"
              },
              "welcomeMessageEnabled": {
                "type": "boolean"
              },
              "iceBreakers": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "commands": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ConversationSettingsCommand"
                }
              }
            },
            "required": [
              "connectionId",
              "phoneNumberId",
              "welcomeMessageEnabled",
              "iceBreakers",
              "commands"
            ]
          },
          "limits": {
            "$ref": "#/components/schemas/ConversationSettingsLimits"
          }
        },
        "required": [
          "data",
          "limits"
        ]
      },
      "ConversationSettingsValidationError": {
        "type": "object",
        "description": "Every problem at once, so a form can show them all.",
        "properties": {
          "error": {
            "type": "string",
            "const": "invalid_conversation_settings"
          },
          "message": {
            "type": "string"
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string",
                  "examples": [
                    "iceBreakers[2]"
                  ]
                },
                "message": {
                  "type": "string"
                }
              },
              "required": [
                "field",
                "message"
              ]
            }
          },
          "limits": {
            "$ref": "#/components/schemas/ConversationSettingsLimits"
          }
        },
        "required": [
          "error",
          "message",
          "issues",
          "limits"
        ]
      },
      "QrCode": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Meta's identifier and the tail of the short link. Immutable."
          },
          "prefilledMessage": {
            "type": "string"
          },
          "deepLinkUrl": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://wa.me/message/ABCDEFGHIJKL1"
            ]
          },
          "qrImageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Rendered image, only when `imageFormat` was requested. It expires: download the bytes rather than printing this URL."
          }
        },
        "required": [
          "code",
          "prefilledMessage",
          "deepLinkUrl"
        ]
      },
      "QrCodeLimits": {
        "type": "object",
        "properties": {
          "prefilledMessageMax": {
            "type": "integer",
            "examples": [
              140
            ]
          },
          "perNumberMax": {
            "type": "integer",
            "examples": [
              2000
            ]
          },
          "analytics": {
            "type": "string",
            "description": "Always states that Meta reports no scan or click data."
          }
        },
        "required": [
          "prefilledMessageMax",
          "perNumberMax",
          "analytics"
        ]
      },
      "QrCodeImageFormat": {
        "type": "string",
        "enum": [
          "SVG",
          "PNG"
        ]
      },
      "QrCodeCreateRequest": {
        "type": "object",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Required with a partner-wide key."
          },
          "prefilledMessage": {
            "type": "string",
            "minLength": 1,
            "maxLength": 140,
            "description": "Text typed (not sent) in the customer's WhatsApp when they open the link. Not blank."
          },
          "imageFormat": {
            "$ref": "#/components/schemas/QrCodeImageFormat",
            "description": "Ask for a rendered QR image. Omit to get only the short link."
          }
        },
        "required": [
          "prefilledMessage"
        ]
      },
      "QrCodeUpdateRequest": {
        "type": "object",
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "Required with a partner-wide key."
          },
          "prefilledMessage": {
            "type": "string",
            "minLength": 1,
            "maxLength": 140,
            "description": "The new prefilled text."
          },
          "imageFormat": {
            "$ref": "#/components/schemas/QrCodeImageFormat"
          }
        },
        "required": [
          "prefilledMessage"
        ]
      },
      "QrCodeListResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QrCode"
            }
          },
          "connectionId": {
            "type": "string"
          },
          "limits": {
            "$ref": "#/components/schemas/QrCodeLimits"
          }
        },
        "required": [
          "data",
          "connectionId",
          "limits"
        ]
      },
      "QrCodeCreateResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/QrCode"
          },
          "connectionId": {
            "type": "string"
          },
          "limits": {
            "$ref": "#/components/schemas/QrCodeLimits"
          }
        },
        "required": [
          "data",
          "connectionId",
          "limits"
        ]
      },
      "QrCodeResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/QrCode"
          },
          "connectionId": {
            "type": "string"
          }
        },
        "required": [
          "data",
          "connectionId"
        ]
      },
      "QrCodeDeleteResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "warning": {
            "type": "string"
          }
        },
        "required": [
          "ok",
          "warning"
        ]
      }
    },
    "responses": {
      "ValidationError": {
        "description": "The request is malformed or incomplete (`missing_fields`, `invalid_*`, …). Fix the request; retrying it unchanged will fail again.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing_fields": {
                "summary": "A required field is missing",
                "value": {
                  "error": "missing_fields",
                  "message": "connectionId and to are required"
                }
              },
              "invalid_media": {
                "summary": "A media object is unusable",
                "value": {
                  "error": "invalid_media",
                  "message": "image requires an \"assetId\" (recommended), an \"id\" or a \"link\""
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No usable API key. Send `Authorization: Bearer pk_live_…` with a key generated in the dashboard under API keys. `invalid_token` covers an unknown or revoked key and a key whose account is suspended.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing_bearer_token": {
                "summary": "No `Authorization: Bearer` header",
                "value": {
                  "error": "missing_bearer_token"
                }
              },
              "invalid_token": {
                "summary": "Unknown or revoked key, or inactive account",
                "value": {
                  "error": "invalid_token"
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "A plan limit stops the action. `plan_limit_messages`: the period's message allowance (numbers paid for × messages per number) is exhausted, or a campaign's whole recipient list would not fit; nothing was sent. `plan_limit_media`: plan storage is full. `plan_limit_numbers`: every number the plan pays for is in use. `subscription_past_due`: the trial or paid period lapsed without renewal. Genuka never charges per message (Meta bills the client's WABA directly); these are subscription allowances.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "plan_limit_messages": {
                "summary": "Message allowance exhausted",
                "value": {
                  "error": "plan_limit_messages",
                  "message": "Monthly message quota reached (45000). Upgrade your plan, add numbers, or wait for the next period."
                }
              },
              "plan_limit_media": {
                "summary": "Media storage full",
                "value": {
                  "error": "plan_limit_media",
                  "message": "Media storage full: 1022 MB of 1 GB used on Starter. Delete files you no longer need, upgrade your plan, or wait — every file is removed 30 days after upload."
                }
              },
              "plan_limit_numbers": {
                "summary": "Every paid number slot is in use",
                "value": {
                  "error": "plan_limit_numbers",
                  "message": "Number limit reached (3). Add numbers to your plan to connect another."
                }
              },
              "subscription_past_due": {
                "summary": "Subscription lapsed",
                "value": {
                  "error": "subscription_past_due",
                  "message": "Your subscription is past due. Renew your plan to continue."
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key is valid but not allowed to do this. `account_deactivated`: the client (company) is suspended; its own key stops working and any request naming it is refused, even with a partner-wide key. `out_of_scope`: a client-scoped key asked for another client's `companyId`. `partner_key_required`: the endpoint needs a partner-wide key. Ids belonging to another client are answered with 404, not 403.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "account_deactivated": {
                "summary": "The client is suspended",
                "value": {
                  "error": "account_deactivated",
                  "message": "Deactivated Account"
                }
              },
              "out_of_scope": {
                "summary": "Client-scoped key, other client",
                "value": {
                  "error": "out_of_scope",
                  "message": "This API key is restricted to another client"
                }
              },
              "partner_key_required": {
                "summary": "Endpoint needs a partner-wide key",
                "value": {
                  "error": "partner_key_required",
                  "message": "This endpoint requires a partner-wide API key"
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist, or exists outside the key's scope (a client-scoped key never sees another client's rows). Codes are `<resource>_not_found`.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "connection_not_found": {
                "summary": "Unknown connection id, or not in scope",
                "value": {
                  "error": "connection_not_found"
                }
              }
            }
          }
        }
      },
      "MetaRejected": {
        "description": "Meta read the request and refused it (a malformed template, a variable without an example, an unprovisioned number, a Flow in the wrong state). `meta` carries Meta's code, details and `traceId`. Fix the payload; the same request will be refused again.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "meta_rejected": {
                "summary": "Template refused by Meta",
                "value": {
                  "error": "meta_rejected",
                  "message": "body_text example count does not match the number of variables",
                  "meta": {
                    "errorClass": "unknown",
                    "retryable": false,
                    "code": 100,
                    "details": "body_text example count does not match the number of variables",
                    "traceId": "AbCdEfGh123"
                  }
                }
              }
            }
          }
        }
      },
      "BadGateway": {
        "description": "Meta's Graph API was unreachable or failed, or a storage read failed on our side. Usually transient: retry with backoff. Note that the CDN in front of the API may replace this JSON body with the plain text `error code: 502`.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "meta_rejected": {
                "summary": "Graph unreachable or failing",
                "value": {
                  "error": "meta_rejected",
                  "message": "Graph API request failed with status 500"
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error. Retry later; quote the `x-request-id` header if it persists.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal_error": {
                "summary": "Unhandled server error",
                "value": {
                  "error": "internal_error",
                  "message": "Unexpected error"
                }
              }
            }
          }
        }
      }
    },
    "headers": {
      "RequestId": {
        "description": "Unique id of this call (UUID), set on every response that went through the API, success or failure. The same id identifies the request in the dashboard under Logs; quote it when asking for support.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pk_live_…",
        "description": "Server-side API key generated in the dashboard under **API keys**, sent as `Authorization: Bearer pk_live_…`. Partner-wide (every client) or scoped to one client. Missing header: `401 missing_bearer_token`; unknown or revoked key: `401 invalid_token`; key of a suspended client: `403 account_deactivated`."
      }
    }
  }
}
