# Exerion Zap: contrato completo API multi-tenant sobre a WhatsApp Cloud API oficial da Meta. Cada número conectado é uma "connection" de um "customer" de uma conta. ## Como chamar - Toda chamada leva `Authorization: Bearer `. Nunca escreva a chave em arquivo versionado, log ou conversa: leia do ambiente. - Respostas em JSON. Não envie `Content-Type: application/json` em requisições sem corpo (a API as rejeita). - Um erro traz `{ statusCode, error, message, code? }`; o `code` (como `connection_type_fixed`, `invalid_origin`, `template_not_synced`) é o que o código deve tratar. ## Rotas ### Customers - `POST /v1/customers/default`: Cria (ou devolve) o customer padrão da conta, para conectar o seu próprio número. - `POST /v1/customers`: Cria um customer: um tenant seu, com o seu identificador (externalId). - `GET /v1/customers`: Lista os customers da conta. - `GET /v1/customers/{id}`: Um customer com as suas conexões e o status de cada uma. ### Setup links - `POST /v1/customers/{id}/signup_links`: Gera o link para o cliente conectar o número (tipo, idioma, redirecionamentos, origens que podem embutir a página). - `GET /v1/setup_links`: Lista os links da conta, com filtros por status e customer. - `GET /v1/customers/{id}/setup_links`: Lista os links de um customer. - `PATCH /v1/setup_links/{id}`: Revoga um link ativo ou muda o prazo, o idioma, os redirecionamentos e as origens. - `POST /v1/connections/{id}/reconnect_links`: Gera o link para o cliente reconectar um número que perdeu o acesso. ### Conexões e mensagens - `GET /v1/connections/{id}`: Uma conexão: número, status, tipo e qualidade (nunca segredos). - `POST /v1/connections/{id}/messages`: Envia uma mensagem (texto, template, mídia ou interativa) por número ou por BSUID. - `GET /v1/connections/{id}/messages`: As últimas 100 mensagens da conexão, com origem, status e preço da Meta. - `POST /v1/connections/{id}/rotate_pin`: Troca o PIN de registro de um número registrado pela plataforma. ### Templates - `GET /v1/connections/{id}/templates`: Os templates do número (a cópia sincronizada), com o status da análise da Meta. - `POST /v1/connections/{id}/templates/sync`: Lê os templates da Meta e atualiza a cópia. - `POST /v1/connections/{id}/templates`: Envia um template novo para a análise da Meta. - `DELETE /v1/connections/{id}/templates/{templateId}`: Apaga um template (só aquele idioma) na Meta e aqui. ### Inbox - `GET /v1/conversations`: As conversas dos seus números, da mais recente à mais antiga, com o que não foi lido e se a janela de 24 horas está aberta. - `GET /v1/conversations/{id}/messages`: As mensagens de uma conversa. - `POST /v1/conversations/{id}/read`: Marca a conversa como lida. - `POST /v1/conversations/{id}/messages`: Responde ao contato: texto com a janela aberta, template a qualquer hora. - `GET /v1/conversations/{id}/templates`: Os templates aprovados da conexão da conversa. - `POST /v1/inbox/tokens`: Gera um token de vida curta para mostrar o inbox dentro do seu app. ### Webhooks - `POST /v1/webhook-endpoints`: Cadastra ou troca a URL que recebe os eventos. - `GET /v1/webhook-endpoints`: A URL cadastrada e o segredo de assinatura. - `POST /v1/webhook-endpoints/rotate-secret`: Gera outro segredo de assinatura (o atual deixa de valer na hora). - `GET /v1/webhook-endpoints/{id}/deliveries`: As tentativas de entrega de eventos, com filtros. - `GET /v1/webhook-endpoints/{id}/deliveries/event-types`: Os tipos de evento que já foram entregues (para filtrar). - `GET /v1/webhook-endpoints/{id}/deliveries/{deliveryId}`: Uma entrega com o corpo enviado e a resposta do seu endpoint. - `POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/redeliver`: Reenvia o mesmo evento como uma entrega nova. ### Conta, uso e chaves - `GET /v1/me`: A conta que a credencial representa. - `PATCH /v1/me`: Corrige o nome ou o e-mail da conta (o e-mail pede a senha; só com sessão do painel, não com API key). - `GET /v1/accounts/{id}`: Os dados da própria conta. - `GET /v1/usage`: O que a conta usou no ciclo contra o que o plano inclui. - `GET /v1/analytics/messages`: Mensagens por período, status, direção, tipo, customer e preço da Meta. - `GET /v1/logs`: Os logs da conta numa lista só: a sua API, as chamadas à Meta, os avisos da Meta e as entregas de webhook. - `GET /v1/brand`: Nome, logo e cores da página de conexão. - `PATCH /v1/brand`: Muda o nome, o logo e as cores da página de conexão. - `POST /v1/api-keys`: Cria uma API key (o valor aparece uma vez). - `GET /v1/api-keys`: Lista as API keys (sem o valor). - `DELETE /v1/api-keys/{id}`: Revoga uma API key. ### Privacidade (LGPD) - `GET /v1/privacy`: O aceite dos Termos e da Política e por quanto tempo cada tipo de dado é guardado. - `PATCH /v1/privacy/settings`: Escolhe por quantos dias o conteúdo das mensagens é guardado (30 a 1825; null volta ao padrão). - `GET /v1/privacy/export`: Os dados da própria conta para levar (sem senha, hashes, tokens nem segredos). - `POST /v1/privacy/contacts/export`: Todas as mensagens da conta com um contato (telefone ou BSUID no corpo), para responder a um titular. - `POST /v1/privacy/contacts/erase`: Tira o número, o BSUID e o conteúdo de um contato das mensagens, das cópias de eventos e do inbox. - `POST /v1/privacy/contacts/block`: Bloqueia um contato (art. 18, IV): suspende o tratamento e mantém o dado; nada é recebido, repassado nem enviado para ele. - `POST /v1/privacy/contacts/unblock`: Retira o bloqueio de um contato (pelo contato, ou pelo id do bloqueio). - `GET /v1/privacy/contacts/blocked`: Os contatos bloqueados, com o número ou o id só pelo final. - `POST /v1/privacy/accept-terms`: Registra o aceite dos Termos e da Política em vigor (só com sessão do painel). - `POST /v1/privacy/account/block`: Bloqueia a conta a pedido do titular: fecha as sessões e as chaves, suspende o tratamento e mantém o dado. Só sessão do painel; só o Encarregado reativa. - `POST /v1/privacy/account/erase`: Encerra a conta e apaga tudo nela. Irreversível; pede a senha e só aceita sessão do painel. ## Eventos de webhook - `whatsapp.messages`: Mensagem recebida ou status de entrega de uma enviada (o objeto da Meta). - `whatsapp.connection.created`: O cliente conectou um número. - `whatsapp.connection.reconnected`: Um número perdido voltou (reconexão ou, em coexistência, a Meta avisou que voltou). - `whatsapp.connection.needs_reattention`: A Meta recusou a autorização do cliente ou o número saiu do aplicativo: gere um link de reconexão. - `whatsapp.connection.disconnected`: O cliente removeu o acesso da plataforma: gere um link de reconexão. - `whatsapp.smb_message_echoes`: O que o cliente escreveu no aplicativo, em um número com coexistência. - `whatsapp.smb_app_state_sync`: Contatos do aplicativo (coexistência), repassados como a Meta os manda. - `whatsapp.setup_link.expired`: Um link de conexão venceu sem ser usado. - `whatsapp.message_template_status_update`: A Meta aprovou, rejeitou, pausou ou apagou um template. - `whatsapp.message_template_quality_update`: A qualidade de um template mudou. - `whatsapp.template_category_update`: A categoria de um template mudou. ## API (todas as respostas são JSON) - `POST /v1/customers/default` → 200 `{ id, displayName, isDefault, connections: [] }`. Cria o customer padrão (nome da conta) se preciso; idempotente. - `POST /v1/customers` `{ externalId, displayName }` → customer. O `externalId` "default_customer" é reservado. `GET /v1/customers` lista os customers da conta. - `POST /v1/customers/{id}/signup_links` → `{ id, status, url, expiresAt, connectionType, allowedConnectionTypes, allowedOrigins, language, successRedirectUrl, failureRedirectUrl }`. Só um link fica ativo por customer: gerar outro revoga o anterior. O corpo é opcional: `{ "connectionType": "DEDICATED_CLOUD_API" | "COEXISTENCE", "allowedConnectionTypes": ["DEDICATED_CLOUD_API", "COEXISTENCE"], "allowedOrigins": ["https://app.seu-sistema.com"], "language": "pt" | "en" | "es", "successRedirectUrl": "https://…", "failureRedirectUrl": "https://…" }`. `connectionType` é o jeito de conectar: `DEDICATED_CLOUD_API` (padrão) leva o número para a API, e ele deixa de funcionar no aplicativo do WhatsApp; `COEXISTENCE` é para o cliente conectar o número que já usa no aplicativo WhatsApp Business e continuar usando o aplicativo. Em coexistência valem os limites da Meta: aplicativo na versão 2.24.17 ou mais nova, vazão fixa de 20 mensagens por segundo, mensagens temporárias, "ver uma vez" e listas de transmissão desativadas, e grupos não sincronizados. `allowedConnectionTypes` deixa o **cliente escolher** na página do link: com os dois tipos, a página pergunta se ele quer o número dedicado ou manter o aplicativo, e o que ele concluir na Meta vira o tipo da conexão (enquanto ele não escolhe, `connectionType` do link é `null`; depois de conectar é o tipo escolhido). Um só tipo em `allowedConnectionTypes` é o mesmo que `connectionType`; mandar os dois com respostas diferentes é 400 `connection_types_conflict`, e uma lista vazia, repetida ou com valor desconhecido também é 400. Com um único tipo permitido, o que o cliente concluir na Meta tem de ser esse, senão o link falha com `connection_type_mismatch`. `allowedOrigins` (até 10, cada um só protocolo, endereço e porta, como `https://app.seu-sistema.com`: sem caminho nem asterisco; `http` só em desenvolvimento) lista as origens do app do cliente **que podem mostrar a página do link dentro dele** (num iframe ou popup) e receber o resultado: sem elas, ninguém pode embutir a página. Para embutir, carregue `/sdk/connect.js` no app e chame `ExerionZapConnect.open({ url, mode: "modal" | "inline" | "popup", container, onReady, onCompleted, onFailed, onClose })` com a `url` que o seu servidor recebeu; `onCompleted({ connection: { connectionId, phoneNumberId, businessAccountId, displayPhoneNumber } })` e `onFailed({ errorCode })` só trazem identificadores e códigos: **confirme a conexão pelo webhook `whatsapp.connection.created`**, não pelo navegador. `PATCH /v1/setup_links/{id}` também aceita `allowedOrigins` (`null` ou `[]` limpa). Sem `language`, a página usa o idioma do navegador de quem abre. As URLs (https; em produção, http é recusado) são para onde a página leva o cliente quando o fluxo termina; sem elas a página só mostra o resultado. No sucesso a URL recebe `setup_link_id`, `status=completed`, `phone_number_id`, `business_account_id` e `display_phone_number`; na falha, `setup_link_id`, `status=failed` e `error_code` (`token_exchange_failed`, `connection_failed`, `phone_number_mismatch`, `connection_type_mismatch`, `link_expired`, `link_revoked` ou `already_used`). Só identificadores vão na URL, nunca token ou segredo; confirme a conexão pelo webhook `whatsapp.connection.created` ou por `GET /v1/customers/{id}`, não só pelo redirecionamento. - `GET /v1/setup_links` (filtros opcionais: `status` = ACTIVE, PROCESSING, USED, FAILED, EXPIRED ou REVOKED; `customerId`; `limit`) e `GET /v1/customers/{id}/setup_links` → lista dos links, do mais novo ao mais antigo, cada um com `{ id, customerId, status, url, createdAt, expiresAt, completedAt, connectionId, errorCode }`. A `url` só vem enquanto o status é ACTIVE. - `PATCH /v1/setup_links/{id}` com `{ "status": "REVOKED" }` revoga um link ativo (409 se já foi usado, expirou ou está em andamento); com `{ "expiresAt": "" }` muda o prazo (no máximo 90 dias a partir de agora); também aceita `language`, `successRedirectUrl` e `failureRedirectUrl` (`null` limpa). Envie `status` sozinho, ou os campos a mudar, nunca os dois juntos. - `POST /v1/connections/{id}/reconnect_links` → o mesmo formato do link de conexão, com `reconnect: { connectionId, phoneNumberId, displayPhoneNumber }`. Só para conexões `NEEDS_REATTENTION` (a Meta recusou a autorização do cliente) ou `DISCONNECTED` (o cliente removeu o acesso); com outro status responde 409 `connection_not_reconnectable`. O corpo opcional é o mesmo (`language`, `successRedirectUrl`, `failureRedirectUrl`); o link sempre pede o tipo da conexão, e um `connectionType` diferente dele, ou um `allowedConnectionTypes` que não seja só o tipo dele (inclusive os dois, para o cliente escolher), responde 400 `connection_type_fixed`. O cliente abre o link, entra de novo com a Meta e escolhe **o mesmo número**: a conexão existente é atualizada (mesmo id, mesmo PIN e mesmo tipo) e um número diferente é recusado com o código `phone_number_mismatch`, sem alterar nada. Gerar o link revoga o link ativo do customer. - `GET /v1/brand` e `PATCH /v1/brand` `{ "name", "logoUrl", "theme" }` → nome, logo (URL https) e cores da página de conexão, para todos os links da conta. As cores são `primaryColor`, `backgroundColor`, `textColor`, `mutedTextColor`, `cardColor` e `borderColor` (hexadecimais, como #1a73e8); as que faltam ficam no padrão. Cores sem contraste suficiente (texto 4,5:1, botão 3:1) são recusadas com 400 `invalid_theme` e a lista `problems`. `theme: null` volta ao visual padrão. - `GET /v1/connections/{id}` → uma conexão (número, status, tipo, qualidade), nunca segredos. - `GET /v1/customers/{id}` → customer com `connections: [{ id, status, displayPhoneNumber, phoneNumberId, wabaId, connectionType }]`. `connectionType` é `DEDICATED_CLOUD_API` ou `COEXISTENCE`. `status` é PENDING, CONNECTED, NEEDS_REATTENTION, DISCONNECTED ou FAILED. - `POST /v1/connections/{id}/messages` `{ to, type, content }` → mensagem `{ id, status, waMessageId }`. `to` são só dígitos, com DDI (ex.: 5511999999999). Um usuário que ativou o nome de usuário no WhatsApp pode não ter número para você: nesse caso envie `recipient` com o identificador dele (BSUID, ex.: `BR.13491208655302741918`) no lugar de `to` (com os dois, a Meta usa o número). Sem `to` nem `recipient`, 400 `recipient_required`. Recebidas e enviadas trazem `contactUserId` quando a Meta o informa, e `fromNumber` pode vir vazio: identifique o contato pelo número **ou** pelo `contactUserId`. Nos webhooks `whatsapp.messages` o BSUID está em `messages[].from_user_id`, `contacts[].user_id` e `statuses[].recipient_user_id`. Para `type: "text"`, `content` é `{ "body": "..." }`; também existem `template`, `media` e `interactive` (`content` segue o corpo da Graph API). Texto livre só é entregue dentro de 24 h depois da última mensagem do contato; fora disso a Meta exige `type: "template"`. - `GET /v1/connections/{id}/messages` → últimas 100 mensagens. Cada uma tem `origin`: `API` (passou pela plataforma), `BUSINESS_APP` (o cliente escreveu no aplicativo, em número com coexistência) ou `HISTORY` (histórico que o cliente aceitou compartilhar ao conectar). Só `API` conta no plano e na analytics. Quando a Meta informa o preço (nos status de enviada e de entregue), a mensagem ganha `pricingCategory` (`marketing`, `utility`, `authentication`, `service`…) e `pricingType` (`regular`: a Meta cobra; `free_customer_service` e `free_entry_point`: gratuita); enquanto a Meta não disse, são `null` (um `pricingCategory` enviado por você no envio vale só como palpite, sem `pricingType`). - Templates (mensagens que a Meta aprova antes do envio; só um template aprovado inicia uma conversa fora da janela de 24 horas): `GET /v1/connections/{id}/templates` (a cópia que temos; `?status=APPROVED` filtra) → `[{ id, name, language, category, status, components, metaTemplateId, rejectedReason, qualityScore }]`; `POST /v1/connections/{id}/templates/sync` lê os templates da Meta e atualiza a cópia → `{ synced, added, removed, complete }` (só remove o que a Meta não tem mais quando leu a lista inteira); `POST /v1/connections/{id}/templates` `{ name, language, category, components }` envia um template novo para análise (`name` em minúsculas, dígitos e _; `language` como `pt_BR`; `category` `UTILITY`, `MARKETING` ou `AUTHENTICATION`; `components` no formato da Meta: `[{ "type": "BODY", "text": "Olá {{1}}", "example": { "body_text": [["Ana"]] } }]`, com `HEADER`, `FOOTER` e `BUTTONS` opcionais) → 201 com o template em `PENDING`; uma recusa da Meta é 400 com `code` `meta_` e a mensagem dela; `DELETE /v1/connections/{id}/templates/{templateId}` apaga na Meta e aqui só aquele idioma (409 `template_not_synced` se ainda não tem o id da Meta: sincronize). Para enviar um aprovado: `POST /v1/connections/{id}/messages` com `{ "to": "5511999999999", "type": "template", "content": { "name": "pedido_saiu", "language": { "code": "pt_BR" }, "components": [{ "type": "body", "parameters": [{ "type": "text", "text": "Ana" }] }] } }`. O resultado da análise chega como os eventos `whatsapp.message_template_status_update` (`data.event`: `APPROVED`, `REJECTED`, `PAUSED`, `DISABLED`…, `data.message_template_id`, `message_template_name` e `data.waba_id`), `whatsapp.message_template_quality_update` e `whatsapp.template_category_update`. - `GET /v1/analytics/messages` (`from`, `to`, `groupBy` = day ou hour, `timeZone`, `customerId` opcional) → totais, série e, em `byPricing`, quantas mensagens a Meta precificou por `category` e `type` (`[{ category, type, n }]`). **Contagem, não valor**: o preço depende do país de quem recebe. - `POST /v1/connections/{id}/rotate_pin` (sem corpo) → 200 `{ rotated: true }`: troca o PIN de registro de um número que a plataforma registrou (só número dedicado, com o token do cliente; na coexistência o PIN é do aplicativo e a resposta é 409 `pin_not_managed`; sem token, 409 `connection_has_no_token`). Nunca devolve o PIN. - `GET /v1/me` → a conta que a credencial representa, `{ id, name, email, plan, status, createdAt, terms }` (`terms.upToDate` diz se a conta aceitou a versão em vigor dos Termos e da Política); `GET /v1/accounts/{id}` devolve o mesmo, só para o id da **própria** conta (404 para outro). - Chaves de API: `POST /v1/api-keys` `{ "label" }` → `{ id, label, key, createdAt }` (o valor de `key`, `wap_…`, só aparece nesta resposta); `GET /v1/api-keys` lista `{ id, label, lastUsedAt, revokedAt, createdAt }` sem o valor; `DELETE /v1/api-keys/{id}` revoga a chave. - `GET /v1/logs` (filtros opcionais: `source` = `API`, `META_API`, `META_WEBHOOK` ou `WEBHOOK_DELIVERY`, vários separados por vírgula; `level` = `INFO`, `WARN` ou `ERROR`, vários separados por vírgula, e `ERROR` sozinho é "só erros"; `from` e `to` em ISO 8601; `q` = palavras da linha ou do caminho; `connectionId`; `customerId`; `limit` até 200; `cursor`) → `{ data: [...], nextCursor }`, do mais novo ao mais antigo, cada linha `{ id, source, level, message, method, path, statusCode, durationMs, connectionId, customerId, metadata, createdAt }`: o que a nossa API respondeu às suas chamadas, o que pedimos à Meta por você, o que a Meta nos mandou e o que o seu endpoint respondeu aos webhooks. São só números, códigos e identificadores (nunca o conteúdo de uma mensagem nem uma credencial), guardados por 3 dias no plano Free, 14 no Pro, 30 no Platform e 90 no Enterprise, com um teto de linhas por plano (as mais antigas saem primeiro). Um valor de filtro desconhecido é 400 `invalid_filter` e um `cursor` que não veio da API, 400 `invalid_cursor`. Ler os logs não gera novas linhas. - Inbox. Uma conversa é o que uma conexão trocou com um contato (o número, ou o BSUID quando não há número); não há tabela de contatos, e o `id` é um valor opaco que a API devolve. `GET /v1/conversations` (filtros opcionais: `connectionId`, `customerId`, `unread=true`, `limit` até 100, `cursor`) → `{ data: [{ id, connectionId, customerId, contact: { number, userId }, lastMessage: { direction, type, preview, status, origin, at }, lastInboundAt, windowOpen, windowClosesAt, unread }], nextCursor }`, da mais recente para a mais antiga. `GET /v1/conversations/{id}/messages` (`limit` até 200, `cursor`) → `{ data: [{ id, direction, type, origin, status, text, error, createdAt }], nextCursor }`, da mais nova para a mais antiga. `POST /v1/conversations/{id}/read` (sem corpo) marca como lida (`unread` volta a 0; só as recebidas com `origin` `API` contam como novas). `POST /v1/conversations/{id}/messages` `{ type: "text" | "template", content }` responde ao contato pela mesma conexão, como `POST /v1/connections/{id}/messages`; texto só com `windowOpen` (o contato escreveu nas últimas 24 horas), senão 409 `window_closed`. `GET /v1/conversations/{id}/templates` lista os templates aprovados da conexão. `POST /v1/inbox/tokens` `{ scope: "account" | "customer" | "connection", customerId?, connectionId?, allowedOrigins?, expiresInSeconds? }` (60 a 3600, padrão 900) → `{ token, url, expiresAt, scope, customerId, connectionId, allowedOrigins }`: um token de vida curta para uma página dentro do seu app; a `url` é a página `/embed/inbox?token=…` do painel, para um iframe, que só as `allowedOrigins` podem embutir. O token só lê e responde as conversas do escopo e **não serve para o resto da API** (nem como sessão); só a conta, com a chave dela, cria tokens. - Privacidade (LGPD). A conta é a **controladora** dos dados dos seus contatos e a plataforma é a operadora. `GET /v1/privacy` → `{ terms: { currentVersion, acceptedVersion, acceptedAt, upToDate }, retention: { messages: { days, choice, defaultDays, minDays, maxDays }, webhookDeliveries: { days }, logs: { days }, setupLinks: { days } } }`. `PATCH /v1/privacy/settings` `{ "messageRetentionDays": 90 }` (30 a 1825; `null` = o padrão da plataforma, 365) escolhe por quanto tempo o conteúdo das mensagens é guardado: passado o prazo, o número, o BSUID e o conteúdo saem da linha, que continua contando no plano. `GET /v1/privacy/export` → os dados da própria conta (sem senha, hashes, tokens nem segredos; as mensagens não vão aí). Para atender um titular: `POST /v1/privacy/contacts/export` `{ "contact": "+55 11 91234-5678", "limit", "cursor" }` → `{ matchedIdentities, messages, nextCursor }` (todas as mensagens da conta com aquele contato, do mais antigo; o contato é um telefone, com a pontuação que for, ou um BSUID, e vai **no corpo, nunca na URL**) e `POST /v1/privacy/contacts/erase` `{ "contact" }` → `{ messages, webhookDeliveries, conversationReads }` (tira o número, o BSUID e o conteúdo das mensagens do contato, das cópias de eventos do webhook e das marcas de leitura; a linha fica, sem dado pessoal; idempotente). **Bloquear** (art. 18, IV) é suspender o tratamento e **manter** o dado: `POST /v1/privacy/contacts/block` `{ "contact", "reason" }` → `{ blocked, identities, messagesHidden }` oculta as mensagens do contato (inbox, lista de mensagens, contagem de contatos; o direito de acesso segue valendo), passa a recusar o envio a ele (409 `contact_blocked`, a Meta não é chamada) e descarta o que a Meta enviar dele (não é guardado nem repassado ao webhook); `POST /v1/privacy/contacts/unblock` `{ "contact" }` ou `{ "id" }` (o id vem da lista) o desfaz; `GET /v1/privacy/contacts/blocked` → `{ data: [{ id, kind, contact, reason, createdAt }] }` com o contato só pelo final. **Só uma pessoa, com a sessão do painel** (uma chave de API recebe 403 `session_required`): `POST /v1/privacy/account/block` `{ "password", "code" }` (bloqueia a própria conta: fecha as sessões e as chaves, nada que a Meta enviar é guardado ou entregue, o dado é mantido; **só o Encarregado reativa**; um agente nunca deve chamá-la), `POST /v1/privacy/accept-terms` (registra o aceite dos Termos e da Política), `PATCH /v1/me` `{ name, email, currentPassword }` (corrige o nome ou o e-mail; o e-mail pede a senha) e `POST /v1/privacy/account/erase` `{ "password" }` (encerra a conta e apaga tudo; **irreversível: um agente nunca deve chamá-la**). - **Quem paga a Meta:** o próprio cliente, direto, na conta do WhatsApp Business dele (`customer_managed`). A plataforma não cobra as mensagens da Meta. Para enviar mensagens cobradas o cliente precisa adicionar uma forma de pagamento à conta dele no Meta Business Suite: avise-o ao mandar o link de conexão. ## Webhooks (se o projeto receber mensagens) - Cada evento chega por POST na URL cadastrada em Webhooks no painel, com corpo `{ "type": "...", "data": { ... } }`. - Eventos: `whatsapp.messages` (mensagens recebidas e status de entrega; `data` é o objeto da Meta), `whatsapp.connection.created` (número conectado), `whatsapp.connection.reconnected` (número reconectado; em coexistência também quando a Meta avisa que o número voltou, `data.reason` = `account_reconnected`), `whatsapp.connection.needs_reattention` (a Meta recusou a autorização do cliente, `data.reason` = `token_rejected`; ou, em coexistência, o cliente tirou o número do aplicativo, `data.reason` = `account_offboarded`), `whatsapp.connection.disconnected` (o cliente removeu o acesso; `data.reason` = `partner_removed`) `whatsapp.smb_message_echoes` (mensagem que o cliente escreveu no aplicativo, em número com coexistência; `data` é o objeto da Meta), `whatsapp.smb_app_state_sync` (contatos do aplicativo, repassados como a Meta os manda e não guardados) e `whatsapp.setup_link.expired`. O histórico de conversas compartilhado não é repassado: fica guardado (`origin` `HISTORY`). Os eventos de conexão trazem `{ customerId, connectionId, wabaId, phoneNumberId, displayPhoneNumber, connectionType, status, reason? }`. Ao receber `needs_reattention` ou `disconnected`, gere um link de reconexão e envie ao cliente. - Valide o cabeçalho `X-WhatsApp-Platform-Signature`: HMAC-SHA256, em hexadecimal, do **corpo bruto** com o signing secret (comparação em tempo constante). O secret vem de `GET /v1/webhook-endpoints` (campo `signingSecret`); guarde em $WHATSAPP_PLATFORM_WEBHOOK_SECRET, nunca no código. `POST /v1/webhook-endpoints/rotate-secret` (sem corpo) gera outro e invalida o atual na hora. - A URL do webhook se cadastra (ou troca) com `POST /v1/webhook-endpoints` `{ "url": "https://…" }`: trocar a URL preserva o segredo de assinatura. - A URL do webhook precisa ser https e pública: em produção, localhost e endereços privados são recusados (para testar localmente, use um túnel https). Redirecionamentos não são seguidos. - Responda 2xx rápido; falhas são reenviadas até 5 vezes (esperas de 2, 4, 8 e 16 s). - Cada tentativa fica registrada: `GET /v1/webhook-endpoints/{id}/deliveries` (filtros opcionais: `eventType`, `status` = DELIVERED, RETRYING, FAILED ou ERROR (tudo o que não foi entregue), `from`, `to`, `limit`) devolve, por tentativa, `{ id, eventType, status, responseStatus, error, durationMs, attempt, maxAttempts, createdAt, nextAttemptAt }`; `GET /v1/webhook-endpoints/{id}/deliveries/{deliveryId}` traz também o `payload` enviado e o `responseBody` (primeiros 2 KB da resposta do endpoint); `GET /v1/webhook-endpoints/{id}/deliveries/event-types` lista os tipos de evento que já foram entregues (para o filtro `eventType`); `POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/redeliver` (sem corpo) reenvia o mesmo evento como uma entrega nova. Use isso para depurar um endpoint que não recebe ou recusa eventos. - `GET /v1/usage` → `{ plan, period, messages: { used, included }, numbers: { used, included }, plans }`: o que a conta usou no ciclo (mês calendário, horário de São Paulo) contra o que o plano inclui (`included: null` = sem limite). É informativo: passar do limite não bloqueia o envio.