I'm trying to integrate WhatsApp Cloud API Flows (without endpoint) into my Node.js backend. My goal is to send a Flow via a template button and receive the form submission response in my /webhook endpoint.
Question
Any help or working example would be highly appreciated!
Flow JSON (used in Meta Flow Builder)
{
"routing_model": {
"ADDITIONAL_INCOME": ["SUMMARY"],
"SUMMARY": []
},
"data_api_version": "3.0",
"version": "7.0",
"screens": [
{
"id": "ADDITIONAL_INCOME",
"title": "Additional Income Sources",
"data": {
"houseLoanInterest": { "type": "number", "__example__": 180000 },
"businessIncome": { "type": "number", "__example__": 30000 },
"savingsInterest": { "type": "number", "__example__": 5000 },
"fdInterest": { "type": "number", "__example__": 8000 },
"giftIncome": { "type": "number", "__example__": 60000 },
"otherIncomes": { "type": "number", "__example__": 10000 }
},
"layout": {
"type": "SingleColumnLayout",
"children": [
{
"type": "Form",
"name": "additional_income_form",
"children": [
{
"type": "TextInput",
"label": "š Home Loan",
"name": "houseLoanInterest",
"input-type": "number",
"required": true
},
{
"type": "TextInput",
"label": "šØāš¼ Business Income",
"name": "businessIncome",
"input-type": "number",
"required": true
},
{
"type": "TextInput",
"label": "šµ Savings Interest",
"name": "savingsInterest",
"input-type": "number",
"required": true
},
{
"type": "TextInput",
"label": "š¦ FD Interest",
"name": "fdInterest",
"input-type": "number",
"required": true
},
{
"type": "TextInput",
"label": "š Gift Income",
"name": "giftIncome",
"input-type": "number",
"required": true
},
{
"type": "TextInput",
"label": "š¦ Other Income",
"name": "otherIncomes",
"input-type": "number",
"required": true
},
{
"type": "Footer",
"label": "Continue",
"on-click-action": {
"name": "navigate",
"next": {
"type": "screen",
"name": "SUMMARY"
}
}
}
]
}
]
}
},
{
"id": "SUMMARY",
"title": "Summary",
"terminal": true,
"layout": {
"type": "SingleColumnLayout",
"children": [
{
"type": "TextHeading",
"text": "All Set!"
},
{
"type": "TextBody",
"text": "Your income details have been collected successfully."
},
{
"type": "Footer",
"label": "Finish",
"on-click-action": {
"name": "complete",
"payload": {
"houseLoanInterest": "${form.houseLoanInterest}",
"businessIncome": "${form.businessIncome}",
"savingsInterest": "${form.savingsInterest}",
"fdInterest": "${form.fdInterest}",
"giftIncome": "${form.giftIncome}",
"otherIncomes": "${form.otherIncomes}"
}
}
}
]
}
}
]
}
------
Backend Code
/send-flow endpoint
app.post('/send-flow', async (req, res) => {
const { phone_number, template_name, flow_token } = req.body;
if (!phone_number || !template_name || !flow_token) {
return res.status(400).json({ error: 'Missing phone_number or template_name or flow_token' });
}
const url = `https://graph.facebook.com/v18.0/${PHONE_NUMBER_ID}/messages`;
const headers = {
Authorization: `Bearer ${WHATSAPP_TOKEN}`,
'Content-Type': 'application/json'
};
const data = {
messaging_product: "whatsapp",
to: phone_number,
type: "template",
template: {
name: template_name,
language: {
code: "en"
},
components: [
{
type: "button",
sub_type: "flow",
index: "0",
parameters: [
{
type: "payload",
payload: flow_token
}
]
}
]
}
};
try {
const response = await axios.post(url, data, { headers });
console.log(`ā
WhatsApp Flow template '${template_name}' sent to ${phone_number}`);
res.status(200).json({ success: true, data: response.data });
} catch (err) {
console.error('ā Failed to send WhatsApp Flow:', err.response?.data || err.message);
res.status(500).json({
error: 'Failed to send WhatsApp Flow',
details: err.response?.data || err.message
});
}
});
-------------
/webhook handler
app.post('/webhook', async (req, res) => {
const data = req.body;
console.log("==== Webhook Data ====");
console.log("š© WhatsApp Flow Webhook:", JSON.stringify(data, null, 2));
res.sendStatus(200);
const message = data?.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
if (message?.interactive?.type === "nfm_reply") {
const phone = message.from;
const flowName = message.interactive.nfm_reply.name;
const responseString = message.interactive.nfm_reply.response_json;
try {
const parsedAnswers = JSON.parse(responseString);
console.log("ā
WhatsApp Flow Submission Received:");
console.log("š Phone:", phone);
console.log("š Flow Name:", flowName);
console.log("š¦ Submitted Data:", parsedAnswers);
} catch (err) {
console.error("ā Failed to parse flow response JSON:", err.message);
}
}
});
---
Webhook Logs
š© WhatsApp Flow Webhook:
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "XXXXXXXXXXXXXXX",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"messages": [
{
"interactive": {
"type": "nfm_reply",
"nfm_reply": {
"response_json": "{\"flow_token\":\"unused\"}",
"body": "Sent",
"name": "flow"
}
}
}
]
},
"field": "messages"
}
]
}
]
}
-----
Problem :
Even though the flow contains a form with required fields (like Home Loan, FD Interest, etc.), I'm only getting this in the webhook:
{ "flow_token": "unused" }
- Expected: Full form submission data as key-value pairs
- Actual: Only flow_token
--------------
What I Tried
- Verified that the flow is published and connected to the template
- Flow submission shows success on WhatsApp
- Confirmed the webhook works and logs other message types correctly
Update the footer of SUMMARY screen to following, it should work
{
"type": "Footer",
"label": "Finish",
"on-click-action": {
"name": "complete",
"payload": {
"houseLoanInterest": "${screen.ADDITIONAL_INCOME.form.houseLoanInterest}",
"businessIncome": "${screen.ADDITIONAL_INCOME.form.businessIncome}",
"savingsInterest": "${screen.ADDITIONAL_INCOME.form.savingsInterest}",
"fdInterest": "${screen.ADDITIONAL_INCOME.form.fdInterest}",
"giftIncome": "${screen.ADDITIONAL_INCOME.form.giftIncome}",
"otherIncomes": "${screen.ADDITIONAL_INCOME.form.otherIncomes}"
}
}
}