List Flow responses
The forms customers submitted to ONE Flow, newest first. The Flow is resolved inside the key’s own WABA first, and that resolution is the only thing that scopes what follows — a Flow of another WABA (including another WABA of the same tenant) answers 404 and no response is ever read.
Responses past their retention window are excluded HERE, not only by the pruning job, so a caller can never read a submission the published window says is gone. contactPhone is null when the phone is no longer ours to publish. externalMessageId is Meta’s wamid and is named as a provider id on purpose — every other id on this surface is local.
notice: "FLOW_RESPONSES_WINDOW_TRUNCATED" appears on the page when the Flow minted more sends than one request will correlate (a 10,000-session ceiling). A short answer that does not say it is short is what makes a caller conclude submissions were lost. Permission flows:read.
Requires a number-scoped key. A tenant-scoped key must name the number with the x-whatsapp-number-id header, or it gets 403 TENANT_SCOPE_NOT_ALLOWED.
media[] carries the files the person ATTACHED inside the form (PhotoPicker / DocumentPicker). They are NOT in response: a picker requires data_exchange, so the references arrive in the request our endpoint decrypts — a different HTTP exchange from the nfm_reply that produced the row — and this array is the re-hosted copy, joined by flowToken. It is the only place a caller can reach the bytes: Meta deletes the original after 20 days. It is ALWAYS an array, [] when nothing was attached. url is a stable link of ours and is present as soon as the row exists, INCLUDING while status is "PENDING". Fetching it before the re-host finishes does not 404: the route holds the request while it waits for the file and then serves it, every later fetch is a redirect, and a wait that runs out answers 404 with a Retry-After header. The value does not change once the bytes land, so it is safe to store on arrival, and status (PENDING/STORED/FAILED) is still how a caller knows whether they have. The ordering is why that link exists: the event is emitted as soon as the submission is stored and the re-host runs after it, so the announcement ordinarily precedes the bytes. url is null only when the deployment has no signing secret configured (it then falls back to the direct object URL once STORED), when Meta sent no media_id to sign an identity over, or on a RELAY_ONLY number where nothing was stored at all — never merely because the transfer is unfinished. Treat it as opaque: fetch it, never construct one. mimeType is sniffed from the decrypted bytes and never from the file name, and it and sizeBytes are null until the file is stored. Neither cdn_url nor any cipher material is ever published.
submitted[] is what the person actually TYPED, screen by screen. ⛔ In a data_exchange Flow it is the ONLY place those answers exist: there response is Meta’s nfm_reply, an envelope carrying the flow_token and nothing else — a caller who opens response on such a Flow finds an empty room and concludes the submission was lost. In a PLAIN Flow, one with no endpoint, the answers DO arrive in response and submitted is [].
Each entry is one data_exchange request META SENT US: screen (the screen the person was on, or null), at (ISO 8601), data (the parsed object) and omitted. It is never what YOUR endpoint replied — extension_message_response is not published anywhere in Pilot Status. And ONLY data_exchange exchanges are captured: INIT hands back the state WE seeded when the Flow was sent, and BACK is navigation, so this list is legitimately SHORTER than the traffic your own endpoint saw. That difference is not a loss.
On THIS endpoint submitted is always an array and never null — [] both when there is nothing and when the number keeps no customer content, because “nothing was captured” and “we could not look” would otherwise be the same value. (The flow.response_received webhook is the opposite, null and never [], because the redactor there blanks a field only when it holds something.) Nothing is ever silently truncated: an entry either carries data with omitted: null, or carries data: null and says WHY. DATA_TOO_LARGE is one over-sized exchange, which keeps its row and its place in the sequence — the ceiling it crossed is counted in BYTES, not characters, so accented text reaches it sooner than its length suggests. LIMIT_REACHED is the per-form ceiling of 50 exchanges: one final marker entry, and nothing after it was stored. UNREADABLE is an exchange that was kept but whose content could not be read back. There is no third shape.
The list is append-only and never de-duplicated: someone who walks back a screen and answers it again produces a second, GENUINE entry, and at is what tells two runs under one flowToken apart. Exchanges are kept 30 days + 1 — one day LONGER than the response row — because the exchange is captured while the person is still filling the form in, minutes BEFORE the submission that creates the row; on an identical window they would expire first, and a response still inside its own retention would publish submitted: [] for its last minutes. On a number with piiMode: "RELAY_ONLY" nothing is stored here at all: not a redacted entry, no entry.
curl --request GET \
--url https://pilotstatus.com.br/v1/flows/{id}/responses \
--header 'x-api-key: <api-key>'import requests
url = "https://pilotstatus.com.br/v1/flows/{id}/responses"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://pilotstatus.com.br/v1/flows/{id}/responses', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://pilotstatus.com.br/v1/flows/{id}/responses",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://pilotstatus.com.br/v1/flows/{id}/responses"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://pilotstatus.com.br/v1/flows/{id}/responses")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://pilotstatus.com.br/v1/flows/{id}/responses")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"responses": [
{
"id": "cmf9z8y7x6w5v4u3t2s1r0q9",
"flowId": "cmf1a2b3c4d5e6f7g8h9i0j1",
"contactPhone": "5564999978874",
"flowToken": "b7c1e4f2a9d84c6e8f0a1b2c3d4e5f60",
"externalMessageId": "wamid.HBgNNTU2NDk5OTc4ODc0FQIAERgSN0E5...",
"response": {
"flow_token": "b7c1e4f2a9d84c6e8f0a1b2c3d4e5f60"
},
"submitted": [
{
"screen": "WELCOME_SCREEN",
"at": "2026-09-01T18:21:44.318Z",
"data": {
"screen_0_nome": "Amanda",
"screen_0_nota": "9"
},
"omitted": null
},
{
"screen": "COMENTARIO_SCREEN",
"at": "2026-09-01T18:22:03.907Z",
"data": null,
"omitted": "DATA_TOO_LARGE"
}
],
"media": [
{
"field": "photo_picker",
"fileName": "documento.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 184320,
"status": "STORED",
"url": "https://media.pilotstatus.com.br/public/tenants/tenant_01HZX.../flow-media/8f2c1a4b9d.jpg"
},
{
"field": "document_picker",
"fileName": "comprovante.pdf",
"mimeType": null,
"sizeBytes": null,
"status": "PENDING",
"url": "https://pilotstatus.com.br/api/flows/media/eyJ0IjoiY21xc2N4MXczIiwiayI6ImZvdG8ifQ.9f2c1a4b8e0d6c73"
}
],
"receivedAt": "2026-09-01T18:22:11.000Z",
"expiresAt": "2026-10-01T18:22:11.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 30,
"totalPages": 1
}{
"error": "Paginação profunda demais: page × pageSize não pode passar de 10000.",
"errorEN": "Pagination too deep: page × pageSize must not exceed 10000.",
"code": "INVALID_PAGINATION"
}{
"error": "Unauthorized"
}{
"error": "Tenant-scoped keys cannot call number endpoints",
"code": "TENANT_SCOPE_NOT_ALLOWED"
}{
"error": "Flow não encontrado para o número desta chave.",
"errorEN": "Flow not found for this key's number.",
"code": "FLOW_NOT_FOUND"
}{
"error": "Flows existem apenas em números Meta (API Oficial). O número desta chave não é Meta ou não tem uma WABA associada — use uma chave de um número Meta.",
"errorEN": "Flows only exist on Meta (Cloud API) numbers. This key's number is not a Meta number, or has no WABA behind it — use a key bound to a Meta number.",
"code": "FLOW_REQUIRES_META_NUMBER"
}{
"error": "Too many requests"
}{
"error": "Erro interno do servidor.",
"errorEN": "Internal server error.",
"code": "INTERNAL_ERROR"
}Authorizations
Your ps_ API key
Path Parameters
The local id of the Flow — the id field GET /v1/flows returns, never metaFlowId.
Query Parameters
Page number, 1-based.
x >= 1Rows per page. Default 30, maximum 100.
1 <= x <= 100Response
List Flow responses
The submissions on this page, newest first.
Show child attributes
Show child attributes
Submissions matching the read across all pages, not the size of this page.
Present ONLY when the Flow minted more sends than one request will correlate (a 10,000-session ceiling); absent otherwise. The page is then SHORT — treat it as "there may be more", never as "these are all the submissions".
FLOW_RESPONSES_WINDOW_TRUNCATED Was this page helpful?
curl --request GET \
--url https://pilotstatus.com.br/v1/flows/{id}/responses \
--header 'x-api-key: <api-key>'import requests
url = "https://pilotstatus.com.br/v1/flows/{id}/responses"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://pilotstatus.com.br/v1/flows/{id}/responses', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://pilotstatus.com.br/v1/flows/{id}/responses",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://pilotstatus.com.br/v1/flows/{id}/responses"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://pilotstatus.com.br/v1/flows/{id}/responses")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://pilotstatus.com.br/v1/flows/{id}/responses")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"responses": [
{
"id": "cmf9z8y7x6w5v4u3t2s1r0q9",
"flowId": "cmf1a2b3c4d5e6f7g8h9i0j1",
"contactPhone": "5564999978874",
"flowToken": "b7c1e4f2a9d84c6e8f0a1b2c3d4e5f60",
"externalMessageId": "wamid.HBgNNTU2NDk5OTc4ODc0FQIAERgSN0E5...",
"response": {
"flow_token": "b7c1e4f2a9d84c6e8f0a1b2c3d4e5f60"
},
"submitted": [
{
"screen": "WELCOME_SCREEN",
"at": "2026-09-01T18:21:44.318Z",
"data": {
"screen_0_nome": "Amanda",
"screen_0_nota": "9"
},
"omitted": null
},
{
"screen": "COMENTARIO_SCREEN",
"at": "2026-09-01T18:22:03.907Z",
"data": null,
"omitted": "DATA_TOO_LARGE"
}
],
"media": [
{
"field": "photo_picker",
"fileName": "documento.jpg",
"mimeType": "image/jpeg",
"sizeBytes": 184320,
"status": "STORED",
"url": "https://media.pilotstatus.com.br/public/tenants/tenant_01HZX.../flow-media/8f2c1a4b9d.jpg"
},
{
"field": "document_picker",
"fileName": "comprovante.pdf",
"mimeType": null,
"sizeBytes": null,
"status": "PENDING",
"url": "https://pilotstatus.com.br/api/flows/media/eyJ0IjoiY21xc2N4MXczIiwiayI6ImZvdG8ifQ.9f2c1a4b8e0d6c73"
}
],
"receivedAt": "2026-09-01T18:22:11.000Z",
"expiresAt": "2026-10-01T18:22:11.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 30,
"totalPages": 1
}{
"error": "Paginação profunda demais: page × pageSize não pode passar de 10000.",
"errorEN": "Pagination too deep: page × pageSize must not exceed 10000.",
"code": "INVALID_PAGINATION"
}{
"error": "Unauthorized"
}{
"error": "Tenant-scoped keys cannot call number endpoints",
"code": "TENANT_SCOPE_NOT_ALLOWED"
}{
"error": "Flow não encontrado para o número desta chave.",
"errorEN": "Flow not found for this key's number.",
"code": "FLOW_NOT_FOUND"
}{
"error": "Flows existem apenas em números Meta (API Oficial). O número desta chave não é Meta ou não tem uma WABA associada — use uma chave de um número Meta.",
"errorEN": "Flows only exist on Meta (Cloud API) numbers. This key's number is not a Meta number, or has no WABA behind it — use a key bound to a Meta number.",
"code": "FLOW_REQUIRES_META_NUMBER"
}{
"error": "Too many requests"
}{
"error": "Erro interno do servidor.",
"errorEN": "Internal server error.",
"code": "INTERNAL_ERROR"
}