Skip to main content
Um Flow é um formulário que abre dentro da conversa do WhatsApp — uma tela de agendamento, um formulário de lead, uma triagem de suporte — em vez de uma conversa que o cliente precisa digitar inteira. O Pilot Status cuida do ciclo completo: criar e editar no painel (/flows), pela API REST (/v1/flows), ou anexar um Flow a um template com um botão FLOW. Flows existem apenas em números Meta (API Oficial). Eles pertencem à conta WhatsApp Business (WABA), não a um número — então todo número daquela WABA envia os mesmos Flows.

Dois tipos de Flow

A diferença é se o seu backend participa enquanto o formulário está aberto. Um Flow NAVIGATE é entregue, preenchido e enviado uma vez. Você recebe a resposta inteira no fim. A maioria dos Flows é assim, e não precisa de nada além do Flow JSON. Um Flow data_exchange responde no meio do formulário: o cliente escolhe a data e a tela seguinte mostra os horários que estão de fato livres. Isso exige um endpoint HTTPS que a Meta chama a cada tela — e a Meta cifra cada uma dessas chamadas.
A Meta não publica um Flow data_exchange sem endpoint_uri registrado — ela responde “Publishing without specifying ‘endpoint_uri’ is forbidden”. Registrar é o passo 3 mais abaixo, e é uma requisição própria.

Ciclo de vida

1

Criar

O Flow nasce em DRAFT. Dê um nome e uma ou mais categorias (SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, OTHER).
2

Subir o Flow JSON

As telas são um documento JSON. A Meta responde 200 com a lista de erros de validação em vez de recusar — então resposta verde não significa que o documento compilou. Leia sempre validationErrors.
3

Publicar

Publicar torna o Flow enviável — e é irreversível do lado da Meta. Um Flow publicado nunca mais pode ser editado.
4

Clonar para fazer uma nova versão

Como publicar é definitivo, a forma de mudar um Flow no ar é clonar. O clone é um Flow novo, com id novo, e o Pilot Status grava a linhagem para você seguir a cadeia de uma versão à seguinte.
5

Depreciar

Aposentar um Flow não o apaga, e não apaga as respostas que já foram enviadas. Você pode nomear o Flow que o substitui, que é o que mantém a cadeia de versões legível.
Publicar não tem volta. Não existe editar nem despublicar — só clonar. O painel pede uma confirmação explícita, e a API exige "confirm": true no corpo, justamente para um clique errado não conseguir fazer isso.

Enviar um Flow

Anexe o Flow a um template usando um botão FLOW. O botão amarra por flow_id, por flow_name ou por flow_json embutido. Todo envio cunha um flow_token novo — um valor de uso único que liga aquele envio específico à resposta que chega depois.

Receber a resposta

Quando o cliente envia, a Meta manda um nfm_reply com o flow_token e, num Flow NAVIGATE, o formulário preenchido como string JSON. Há duas formas de ler:
  • Ao vivo — o evento de webhook flow.response_received, já com as respostas parseadas. Veja Receber as respostas de um Flow.
  • Depois — GET /v1/flows/{id}/responses, já ligado ao contato que respondeu.
A mensagem de chat que carrega uma submissão não tem resposta nenhuma dentro, e o evento precisa ser assinado. O nfm_reply da Meta põe um rótulo seco no corpo da mensagem — Enviado / Sent — e é esse rótulo que chega em content, no GET /v1/messages e no espelho do Chatwoot. Um webhook assinado só em messages recebe, portanto, uma mensagem que parece vazia, sem erro em lugar nenhum. Acrescente flow.response_received ao events dele.
Num Flow data_exchange as respostas não estão no response, ponto. O nfm_reply da Meta carrega o flow_token e mais nada, então o response — no evento e na superfície de leitura — traz um token seco enquanto o seu próprio endpoint recebeu o que a pessoa de fato digitou. Essas respostas são publicadas em submitted, uma entrada por troca. O response só mantém o significado antigo num Flow sem endpoint, onde os componentes de texto livre continuam chegando nele.
As respostas ficam guardadas por uma janela de retenção e são podadas depois. A janela é aplicada na leitura, então uma resposta vencida nunca é servida — mesmo que o podador ainda não tenha chegado nela.

data_exchange: o Pilot Status cuida da criptografia

Essa é a parte que costuma impedir os times de colocar um Flow interativo no ar. A Meta cifra cada chamada ao endpoint: um payload AES-GCM cuja chave chega embrulhada em RSA-2048/OAEP-SHA-256 sob a sua chave pública, e a resposta tem de voltar cifrada com a mesma chave AES sob um IV invertido bit a bit. Errar qualquer detalhe disso produz um Flow que simplesmente não avança, sem erro nenhum dos dois lados. Então o endpoint é o Pilot Status. A gente guarda a chave, decifra a requisição da Meta e repassa JSON puro por HTTPS para um webhook seu. A sua resposta é cifrada na volta. Você nunca implementa RSA, AES-GCM nem o IV invertido.
Duas configurações em dois níveis, e um registro — a chave é do número, a URL de encaminhamento é do Flow, e o endpoint_uri é escrito na Meta. Flows NAVIGATE não precisam de nenhum dos três. Os três ficam na tela de Flows do painel, e cada um tem superfície própria na API pública: GET / POST /v1/numbers/{id}/flow-endpoint-key, GET / PUT / PATCH /v1/flows/{id}/endpoint, e POST /v1/flows/{id}/endpoint para o registro — todas atrás de flows:manage. Nenhuma das URLs aparece no GET /v1/flows, porque o token dentro dela é uma credencial — veja Conecte a sua API a um Flow.
1

1. Registrar uma chave para o número

Ou gere uma — o Pilot Status cria um par RSA-2048, guarda a metade privada cifrada e registra a pública na Meta para aquele número — ou importe a chave privada que você já usa, o que não muda nada na Meta. Gerar substitui a chave que a Meta tem, porque a Meta guarda exatamente uma por número.
2

2. Configurar a URL do seu webhook no Flow

É para onde repassamos o JSON decifrado. Fica configurado por Flow, porque um número serve vários formulários e cada um costuma ser um serviço diferente do seu lado.
3

3. Registrar a gente na Meta

Registar na Meta, no painel, escreve a nossa URL de endpoint no endpoint_uri do Flow, na Meta — assim como o POST /v1/flows/{id}/endpoint. Essa URL tem dois segmentos — um identificando o número, outro identificando o Flow — porque a requisição da Meta não diz a qual Flow ela pertence. Salvar a URL no passo 2 não faz isso por você, de propósito: como efeito colateral de um rename, sequestraria um Flow que já aponta para o endpoint de outra pessoa. Um Flow que já aponta para nós é respondido sem escrita; um que aponta para outro lugar é recusado até você confirmar que quer assumir.

O que o seu webhook recebe

action é INIT quando o formulário abre, BACK quando o cliente volta, e data_exchange quando ele envia uma tela.
Se data chega vazio, a causa quase sempre é um caractere no seu Flow JSON.Um valor de formulário é referenciado pela palavra-chave literal form — ${form.quantidade} — e o name que você deu ao componente Form nunca entra na referência. Com um Form chamado form_produto, continua sendo ${form.quantidade}, nunca ${form_produto.quantidade}.A Meta não recusa um documento com o prefixo errado. Ela compila, publica, não acusa erro de validação — e em execução não resolve nada e manda "data": {} ao seu endpoint. A pessoa vê apenas “Could not complete this step. Please try again.”, que não nomeia nada.Desde 12 de setembro de 2026 o POST /v1/flows/{id}/json recusa esse documento em vez de enviá-lo, respondendo 422 FLOW_JSON_DEAD_FORM_PREFIX com a substituição pronta de cada referência. Essa checagem só alcança documentos enviados por nós — um Flow já publicado por fora continua a precisar de correção e republicação.
A requisição vai assinada com HMAC-SHA256 do corpo cru no cabeçalho x-pilot-status-signature — o mesmo cabeçalho e o mesmo esquema dos webhooks de saída do Pilot Status, então quem já valida aqueles não precisa de um segundo caminho.

O que o seu webhook precisa devolver

JSON puro, nomeando a próxima tela e os dados dela:
Para encerrar o Flow, devolva a tela reservada SUCCESS:
Responda em poucos segundos. A Meta segura a requisição aberta enquanto uma pessoa olha para um spinner, e não há retentativa — resposta lenta é tela falhada. Faça o trabalho demorado depois de responder, não antes.

O que fica guardado das trocas

Toda troca data_exchange que deciframos fica guardada ao lado da resposta que o formulário acaba produzindo, e é publicada em submitted — uma entrada por troca, tanto no evento flow.response_received quanto no GET /v1/flows/{id}/responses. Dentro do Pilot Status, é o único lugar onde as respostas de um Flow data_exchange existem: o nfm_reply que a Meta manda no fim carrega o flow_token e mais nada, então o response fica vazio justamente nos Flows cujas telas mais coletaram. O que fica guardado é a requisição que a Meta manda para nós — o que a pessoa digitou, tela a tela. O que o seu endpoint respondeu, não: o data que você devolve, extension_message_response incluído, é cifrado de volta para a Meta e não é anotado em lugar nenhum. INIT e BACK também não — o INIT devolve os params que semeamos no envio e o BACK é navegação, então nenhum dos dois é algo que uma pessoa digitou, e a lista é legitimamente mais curta que os traços do seu próprio endpoint.
Nada some em silêncio. Uma troca grande demais para guardar mantém o lugar dela na lista e diz DATA_TOO_LARGE; uma que não conseguimos ler diz UNREADABLE; o teto de 50 trocas por formulário fecha a lista com uma entrada final dizendo LIMIT_REACHED, e nada depois dela foi guardado. A lista também é append-only: quem volta uma tela e envia de novo produz uma segunda entrada legítima, separada da primeira só pelo horário.
Um número que não guarda conteúdo de cliente não guarda nada disso. Em RELAY_ONLY não existe entrada nenhuma — nem redigida — e o submitted vem vazio, que é também o que a redação deixa para trás. Vazio nunca é prova de que ninguém digitou nada.⚠️ E “vazio” escreve-se de forma diferente nas duas superfícies, de propósito. Em GET /v1/flows/{id}/responses o submitted é [] e nunca null: o valor vem de uma busca que não responde nada quando não há trocas, e um campo posto a nada desapareceria do JSON em vez de chegar vazio. No evento flow.response_received é null e nunca []: aquele payload é guardado, o redator só apaga um campo que tenha algo dentro, e um array vazio voltaria como o marcador de “conteúdo indisponível” e seria lido como respostas que foram escondidas. Cada superfície escolheu a resposta certa para o problema dela.As 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 uma janela idêntica, as trocas venceriam primeiro e uma resposta ainda sendo servida responderia com um submitted vazio ao lado de tudo o que ela ainda tinha.
A forma campo a campo — screen, at, data, omitted — está na API de Flows.

Quando o seu webhook falha

Se o seu servidor está fora, estoura o tempo ou responde algo inutilizável, o Pilot Status não repassa a falha como tela quebrada. O cliente vê um erro genérico na tela em que já está, e pode tentar de novo — nada do que ele digitou se perde, e nada sobre a sua infraestrutura é mostrado a ele.
Duas verificações de saúde são tratadas por nós e nunca chegam ao seu webhook:
  • O ping da Meta — respondido pelo Pilot Status, para o seu servidor estar dormindo não fazer a Meta marcar o endpoint como doente para todo Flow do número.
  • Chave divergente — se a cópia da Meta um dia sair de sincronia com a nossa, o endpoint manda a Meta rebuscá-la. Esse é o único significado daquela resposta, então falha do seu lado nunca é reportada como problema de chave.

Anexos, e o que a Meta não deixa você desenhar

Um PhotoPicker ou um DocumentPicker coloca um arquivo na submissão. Quatro restrições decidem se o Flow que você está desenhando é construível:
O limite de um picker é da Meta, e estas são as palavras dela. Subir um Flow JSON com um PhotoPicker e um DocumentPicker na mesma tela é recusado com 422 FLOW_JSON_INVALID, e a entrada da Meta volta verbatim dentro de validationErrors:
pointers nomeia os componentes recusados. Mover o segundo picker para uma tela só dele faz o mesmo documento compilar, com validationErrors: []. No SDK de Node esse mesmo array se lê com flowJsonValidationErrors(err).Medido contra a Meta em 6 de setembro de 2026. O código, o texto e o limite são da Meta e ela pode mudá-los sem avisar ninguém — se isto deixar de bater com o que você recebe, acredite na resposta da Meta, não nesta página.
O arquivo re-hospedado fica numa URL pública: quem tem o link lê o arquivo. É uma decisão explícita do dono — a URL é o controle de acesso, sem nada na frente dela — e é a mesma propriedade que o mediaLink de entrada já tem. Decida isso antes de um Flow seu pedir documento de identidade a alguém, e trate as URLs como trataria os arquivos.O tipo de conteúdo é decidido pelos bytes, nunca pelo nome do arquivo, e nunca é text/html nem image/svg+xml mesmo quando os bytes parecem um deles — os dois executariam script na origem que os serve.A cópia não vence, e sobrevive à resposta a que ela pertence. As respostas, as sessões e os traços do endpoint são podados por uma janela de retenção; o arquivo re-hospedado não — ele não tem expiração, e nada na plataforma o apaga. As respostas param de ser servidas no GET /v1/flows/{id}/responses no prazo; o link para o documento de identidade continua funcionando depois delas.
O contrato de wire — flowMedia no webhook, media na superfície de leitura, e por que a URL já existe antes dos bytes — está em Receber as respostas de um Flow.

Saúde e estado

Cada Flow carrega um estado (DRAFT, PUBLISHED, DEPRECATED, BLOCKED, THROTTLED), a versão do Flow JSON que a Meta compilou, e um veredito de saúde.
A Meta descontinuou a Flows Metrics API em 30/04/2026, sem sucessor. O webhook flows passou a ser o único canal que reporta um Flow sendo limitado ou uma chave pública vencendo — o Pilot Status assina esse webhook e grava o que ele diz no Flow, para o sinal sobreviver à janela dos logs.

Relacionado