x-api-key) cujo número seja Meta (API Oficial). A WABA é DERIVADA desse número — não existe parâmetro para nomear uma, de propósito.
flows:read é um slug próprio e não é templates:read. A listagem de templates devolve texto que o seu workspace escreveu; a resposta de um Flow devolve o que um cliente digitou num formulário — um nome, um telefone, às vezes um documento. As duas coisas são gateadas separadamente de propósito.Listar Flows
id é o id local e é o único id que você manda de volta para nós. metaFlowId é o da Meta e é somente leitura — vem null enquanto o Flow existe aqui e ainda não chegou à Meta.Criar ou clonar
201 com o Flow novo em DRAFT. Omita cloneFromFlowId para começar do zero.
Renomear e recategorizar
name e categories. Nunca publica e nunca deprecia — essas têm endpoint próprio, para um engano num formulário não alcançar uma operação irreversível.
Subir o Flow JSON
200 significa que o documento compilou, e aí validationErrors vem sempre vazio:
422 FLOW_JSON_INVALID, com a lista dela anexada:
A única recusa que é nossa: FLOW_JSON_DEAD_FORM_PREFIX
Um valor de formulário é referenciado pela palavra-chave literal form — ${form.quantidade}. O name do componente Form que envolve os campos nunca entra na referência: com um Form chamado form_produto, continua sendo ${form.quantidade}.
A Meta ACEITA um documento com o prefixo errado, responde validation_errors: [], publica — e em execução manda "data": {} ao seu endpoint, onde o único sintoma é o telefone da pessoa dizendo “Could not complete this step. Please try again.” Desde 12 de setembro de 2026 esta rota recusa esse documento em vez de enviá-lo:
deadRefs traz a substituição pronta, então o conserto é colar em vez de aplicar uma regra à mão.
Estreita de propósito, e um
200 aqui não significa “o documento é válido”. A checagem só dispara quando o root de uma referência coincide com o name de um Form declarado no MESMO documento — esse caso não resolve, nunca. Um root que não reconhecemos passa: a nossa ignorância não pode virar erro seu. Todo o resto continua sendo a Meta a julgar, e continua chegando como FLOW_JSON_INVALID.Cada item de
validationErrors é da Meta, verbatim — error, error_type, message e pointers nomeando os componentes recusados. Um que você vai encontrar se um Flow coleta arquivos é o COMPONENT_TYPE_MAX_NUMBER_PER_SCREEN: no máximo um PhotoPicker ou DocumentPicker por tela. Veja Flows com ele citado por inteiro. O SDK de Node lê o array com flowJsonValidationErrors(err).Publicar
Depreciar
supersededByFlowId é opcional — mas, se você mandar, tem de nomear um Flow real seu: string vazia é recusada em vez de ignorada em silêncio, porque cadeia de versões rompida é invisível depois.
Apagar
DRAFT pode ser apagado. Qualquer outro estado devolve 409.
Ler as respostas dos formulários
data_exchange as respostas chegam tela a tela, e é em submitted que elas ficam — uma entrada por troca, da mais antiga para a mais nova:
A lista é mais curta do que as trocas que o seu endpoint respondeu, e está certa assim. Só as trocas
data_exchange são capturadas. O INIT carrega o estado que nós semeamos — os params mandados no envio, devolvidos de volta — e o BACK é navegação; nenhum dos dois é algo que uma pessoa digitou. Contar as entradas contra os seus próprios traços e reportar a diferença como perda é justamente o engano que este parágrafo existe para evitar.É a requisição que a Meta manda para nós, nunca o que o seu endpoint respondeu. O data que você devolve no SUCCESS, extension_message_response incluído, é cifrado de volta para a Meta e não é anotado em lugar nenhum do Pilot Status — nem aqui, nem em nenhuma outra superfície.É append-only, e não há deduplicação. Quem volta uma tela e envia de novo produz uma segunda entrada legítima em vez de substituir a primeira, e o at é o que separa duas passagens sob um mesmo flow_token.Um número em
RELAY_ONLY não guarda nada aqui — nem entrada redigida, entrada nenhuma: não existe cópia guardada para redigir.Estas entradas são podadas em 30 dias + 1, e o dia a mais não é arredondamento. Uma troca é capturada enquanto a pessoa ainda está preenchendo o formulário, minutos antes da submissão que cria a resposta ao lado dela. Com 30 dias idênticos, as trocas venceriam primeiro, e uma resposta ainda sendo servida responderia submitted: [] — indistinguível de um formulário em que ninguém digitou nada.media — os arquivos anexados dentro do formulário, re-hospedados por nós:
Aqui
media é sempre um array — vazio quando não houve anexo — e isso deliberadamente não é o que o webhook faz. O flowMedia do flow.response_received é null em vez de [], porque o redator de armazenamento só troca um campo que tem algo dentro, e um array vazio seria lido como “houve anexos e nós escondemos”. Ela também traz sizeBytes, que o evento não traz.[] não é prova de que não havia arquivo. O join dos anexos é best-effort de propósito — um anexo que não conseguimos ler custa um campo, nunca a página — então um join que falha deixa todas as linhas daquela página com media: [], ou seja, “não conseguimos olhar” servido como “não tinha nada”. Reconcilie um Flow que coleta arquivos contra algo que você mesmo guardou.É aqui que o registro é definitivo — a URL já era definitiva no evento. A re-hospedagem é assíncrona: o evento é emitido assim que a submissão é gravada, e a transferência roda depois dele, então o evento normalmente chega antes de os bytes estarem no lugar. O url é um link para uma rota nossa e está presente durante tudo isso, também numa linha PENDING — buscá-lo espera pelo arquivo e então o entrega, e só responde 404 com o cabeçalho Retry-After se essa espera estourar. Ou seja: releia este endpoint para ter status, mimeType e sizeBytes, não para obter uma URL que o evento já te deu. O url só vem null quando a instalação não tem segredo de assinatura (aí ela cai na URL direta do objeto assim que o status for STORED), quando a Meta não mandou media_id, ou num número RELAY_ONLY.Esta é a cópia armazenada, não a ao vivo. Para receber as submissões conforme chegam, assine o evento de webhook
flow.response_received — as respostas vêm nele, já parseadas. A mensagem de chat que carrega uma submissão traz só o rótulo da Meta (Enviado / Sent), então um webhook assinado apenas em messages não vê nada. Veja Receber as respostas de um Flow.Chave do endpoint data_exchange
Só é necessária para Flows cujas telas chamam o seu backend — veja Conecte a sua API a um Flow para o quadro completo.
{numberId} é o número ao qual a sua chave está amarrada. A chave privada nunca sai do Pilot Status e não é devolvida por endpoint nenhum; endpointUrl é a base para registrar no seu Flow, com o id do Flow acrescentado como segmento final.A chave é por número; a URL de encaminhamento é por Flow. As duas têm superfície aqui — a chave logo abaixo, a URL em Endpoint do Flow — e nenhuma das duas aparece no GET /v1/flows, de propósito: uma chave de API pode ser uma aplicação de terceiros, e o endereço do seu webhook interno não é algo que uma integração somente-leitura tenha direito de receber.As duas rotas agem sobre o número da própria chave, e o
{numberId} do caminho tem de bater com ele. Uma chave escopada por número já nomeia o seu; uma chave de conta precisa estreitar com x-whatsapp-number-id. Um id de caminho que não bate responde 404 FLOW_NUMBER_NOT_FOUND — nunca 403, e o mesmo corpo quer o id seja de outro tenant, quer seja de outro número seu, para que um palpite errado não aprenda nada.uploadedAt é null quando um par foi gerado aqui mas a Meta nunca aceitou. Isso não é o mesmo que não ter chave, e é o estado que um POST repetido RETOMA em vez de substituir.Ler o estado atual
Gerar, rotacionar ou importar
UmPOST, dois verbos. A presença de privateKey decide qual, e eles não são intercambiáveis: gerar registra uma chave pública nova na Meta e substitui o que estava lá; importar não registra nada.
Só
confirm, privateKey e passphrase são aceitos; qualquer outro campo é recusado, nunca ignorado. A resposta é a forma de leitura acima mais "replaced", que diz se uma chave que estava viva acabou de ser deslocada — um 200 sozinho não diz.
Uma importação é verificada antes de ser guardada. Derivamos a metade pública da chave enviada e comparamos com a da Meta: coincidiu, é guardada e carimbada (
metaStatus: "VALID"); divergiu, é recusada de saída com FLOW_ENDPOINT_KEY_IMPORT_MISMATCH e nada é gravado; se não conseguimos fazer a chamada ao Graph, a chave fica guardada com uploadedAt: null e metaStatus UNKNOWN/NOT_SET — que é o estado “não confirmada”, não uma falha.Endpoint do Flow
Para onde as trocas já decifradas de um Flowdata_exchange são encaminhadas. Por Flow, flows:manage em todos os verbos inclusive no GET, e {flowId} é o id local do GET /v1/flows.
Um caminho, dois atos diferentes. O GET lê esse estado e o PUT / PATCH guardam o seu destino; nenhum dos três fala com a Meta. O POST registra o nosso proxy como endpoint_uri do Flow na Meta.
Campos do corpo:
url (obrigatório, https:// ou um null explícito para limpar — nunca omitido) e rotateSecret (booleano). PUT e PATCH são a mesma operação. O número vem da credencial e é recusado no corpo (FLOW_NUMBER_FROM_KEY).Salvar a URL não registra nada na Meta — isso é um ato separado sobre um sistema separado, para que um rename não sequestre em silêncio um Flow já apontado para o endpoint de outra pessoa. metaEndpointUri, drift e warnings são como você enxerga a diferença.Registrar o endpoint_uri na Meta
Um POST no mesmo caminho escreve endpointUri no endpoint_uri do Flow na Meta. O corpo é opcional; confirmOverwrite (booleano) é o único campo aceito, e o número vem da credencial como em todo o resto. A resposta é o mesmo estado acima sem secret — o POST nunca cria nenhum.
É idempotente. Um Flow que já aponta para nós responde
200 sem escrever nada na Meta. A verificação lê a Meta ao vivo em vez da coluna metaEndpointUri sincronizada de hora em hora, porque essa coluna é muda sobre uma URI registrada na última hora — exatamente a janela em que duas pessoas configuram o mesmo Flow.Erros
Toda falha desta superfície responde o mesmo envelope — existe exatamente uma forma para ler, e uma grafia:FLOW_JSON_INVALID acrescenta um array validationErrors; FLOW_JSON_DEAD_FORM_PREFIX acrescenta deadRefs; FLOW_UNKNOWN_FIELDS acrescenta as chaves recusadas; FLOW_ENDPOINT_URI_WOULD_OVERWRITE acrescenta currentEndpointUri. Nunca assuma que o conjunto de códigos é fechado — trate os que você conhece e caia no code para o resto.
Os dois últimos não têm prefixo
FLOW_ — são compartilhados por toda a API pública e mantêm a grafia global. Um cliente que filtre por code.startsWith("FLOW_") perderia os dois.