Zie619/n8n-workflows · error · Error
Flow token is missing.
Error message
Flow token is missing.
What it means
Thrown by the 'Encrypt Return' Code node when `$('Json Parser').first().json.flow_token` is empty. flow_token is a WhatsApp Flows field that identifies the flow session and is mandatory in every data_exchange response; without it the encrypted response would be rejected by WhatsApp.
Source
Thrown at workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json:220
-1320,
860
],
"parameters": {
"jsCode": "const crypto = require(\"crypto\");\n\nconst privateKey = `-----BEGIN PRIVATE KEY-----\n[INSERT YOUR KEY HERE]\n-----END PRIVATE KEY-----`;\n\n// Convert input buffers\nconst encryptedAesKeyBuffer = Buffer.from($json.encryptedAesKey.data);\nconst initialVector = Buffer.from($json.initialVector.data);\nconst encryptedFlowData = Buffer.from($json.encryptedFlowData.data);\n\n// Check if encrypted AES key, IV, and encrypted flow data exist\nif (!encryptedAesKeyBuffer || !initialVector || !encryptedFlowData) {\n throw new Error(\"Missing required data (encrypted AES key, IV, or flow data)\");\n}\n\n// Decrypt AES key using RSA\nconst decryptedKey = crypto.privateDecrypt(\n {\n key: privateKey,\n padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,\n oaepHash: \"sha256\",\n },\n encryptedAesKeyBuffer\n);\n\n// Ensure AES key is exactly 16 bytes (AES-128 requires it)\nconst aesKey = decryptedKey.slice(0, 16);\nif (aesKey.length !== 16) {\n throw new Error(\"Invalid AES Key length\");\n}\n\n// Handle initialization vector (IV): If needed, flip the IV bits (standardize behavior)\nconst standardizedIv = Buffer.from(initialVector);\nif (standardizedIv.length !== 16) {\n throw new Error(\"Invalid IV length, must be 16 bytes\");\n}\n\n// Extract the last 16 bytes as the authentication tag (GCM uses 16-byte tags)\nconst authTag = encryptedFlowData.slice(-16);\nconst encryptedDataWithoutTag = encryptedFlowData.slice(0, -16);\n\n// AES Decryption\nconst decipher = crypto.createDecipheriv(\"aes-128-gcm\", aesKey, standardizedIv);\ndecipher.setAuthTag(authTag);\n\nlet decrypted;\ntry {\n decrypted = Buffer.concat([\n decipher.update(encryptedDataWithoutTag),\n decipher.final(),\n ]);\n} catch (error) {\n throw new Error(\"Decryption failed: \" + error.message);\n}\n\nreturn [{ \n decryptedPayload: decrypted.toString(\"utf-8\"),\n aesKey: aesKey.toString(\"base64\")\n}];\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},
{
"id": "17c055f3-c278-48c4-89d4-d305a35bc526",
"name": "Encrypt Return",
"type": "n8n-nodes-base.code",
"position": [
-200,
760
],
"parameters": {
"jsCode": "const crypto = require(\"crypto\");\n\n// Access initial_vector from the correct path\nconst initialVector = $('move to base64').first().json.initialVector;\n\nif (!initialVector) {\n throw new Error(\"Initial Vector is undefined or missing.\");\n}\n\n// Check if 'data' is a property of initialVector\nconst ivData = initialVector.data || initialVector; // Fallback to initialVector if no 'data' property\n\nif (!ivData) {\n throw new Error(\"Initial Vector 'data' is undefined or missing.\");\n}\n\n// Check for various formats of initialVector\nlet ivBuffer;\nif (typeof ivData === \"string\") {\n ivBuffer = Buffer.from(ivData, 'base64');\n} else if (Buffer.isBuffer(ivData)) {\n ivBuffer = ivData;\n} else if (Array.isArray(ivData)) {\n ivBuffer = Buffer.from(ivData);\n} else {\n throw new Error(\"Initial Vector 'data' is in an unsupported format.\");\n}\n\n// Invert Initialization Vector\nconst invertedIV = Buffer.from(ivBuffer.map((b) => ~b & 0xFF)); // Ensure the result stays a valid byte\n\n// Access AES Key from the correct path\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) {\n throw new Error(\"AES Key is missing.\");\n}\n\nconst aesKey = Buffer.from(aesKeyBase64, \"base64\");\n\n// Extract data from the input with proper error handling\nlet date = \"2025-03-14\"; // Default fallback date\nlet startTimes = []; // Default empty array for start times\n\n// Check if $json exists and has the expected structure\nif ($json) {\n // Check if $json is an array\n if (Array.isArray($json) && $json.length > 0) {\n const appointmentData = $json[0];\n if (appointmentData && appointmentData.appointment_date) {\n date = appointmentData.appointment_date;\n }\n if (appointmentData && Array.isArray(appointmentData.start_times)) {\n startTimes = appointmentData.start_times;\n }\n } else if ($json.appointment_date) {\n // If $json is not an array but has appointment_date directly\n date = $json.appointment_date;\n if (Array.isArray($json.start_times)) {\n startTimes = $json.start_times;\n }\n }\n}\n\n// Log the structure of $json for debugging\nconsole.log(\"Input JSON structure:\", JSON.stringify($json, null, 2));\n\n// Ensure we have time slots (use defaults if none found)\nif (!startTimes.length) {\n console.log(\"No time slots found in input, using defaults\");\n startTimes = [\"12:00:00\", \"12:30:00\", \"13:30:00\", \"14:00:00\"];\n}\n\n// Map the time slots to the required format\nconst timeSlots = startTimes.map((timeString, index) => ({\n id: `time_${index + 1}`,\n title: timeString\n}));\n\n// Map the date slots for each time slot\nconst dateSlots = [{\n id: \"date_1\",\n title: date\n}];\n\n// Define the response data with the extracted time and date\nconst responseData = {\n status: \"active\",\n time: timeSlots,\n date: dateSlots\n};\n\n// Define the flow_token (accessed from the correct path)\nconst flowToken = $('Json Parser').first().json.flow_token || \"\"; // Fetch the flow_token dynamically from the path\n\nif (!flowToken) {\n throw new Error(\"Flow token is missing.\");\n}\n\n// Define the next screen (this should be based on your flow logic)\nconst nextScreen = \"APPOINTMENT\"; // You can set this dynamically depending on the flow\n\n// Define Response Message (updated to match the required response format)\nconst responseMessage = JSON.stringify({\n version: \"3.0\", // Fixed version as per your requirements\n action: \"data_exchange\", // Since we're responding to a data exchange request\n screen: nextScreen, // The next screen that the user will be redirected to\n data: responseData, // Data to send back (includes the time and date)\n flow_token: flowToken, // Flow token for session identification\n});\n\n// Encrypt Response using AES-GCM\nconst cipher = crypto.createCipheriv(\"aes-128-gcm\", aesKey, invertedIV);\nlet encryptedResponse = Buffer.concat([\n cipher.update(responseMessage, \"utf-8\"),\n cipher.final()\n]);\n\n// Get the authentication tag\nconst authTag = cipher.getAuthTag();\n\n// Append the authentication tag to the encrypted response\nconst result = Buffer.concat([encryptedResponse, authTag]);\n\n// Encode the entire response as Base64\nconst base64Response = result.toString(\"base64\");\n\n// Return the Base64-encoded response as the body\nreturn [{ body: base64Response }];\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},
{
"id": "412f55e3-5867-4e65-a494-3e3bf991d59c",
"name": "Encrypt Return1",
"type": "n8n-nodes-base.code",
"position": [
-200,
1000
],
"parameters": {
"jsCode": "const crypto = require(\"crypto\");\n\nconst jsonData = items;\n\n// Parse the decryptedPayload string into a JSON object\nconst decryptedPayload = JSON.parse(jsonData[0].json.originalPayload.decryptedPayload);\n\n// Extract the seats array\nconst seats = decryptedPayload.data.seats;\n\nif (!seats || !Array.isArray(seats) || seats.length === 0) {\n throw new Error(\"Seats data is missing or invalid.\");\n}\n\n// Access initial_vector from the correct path\nconst initialVector = $('move to base64').first().json.initialVector;\nif (!initialVector) {\n throw new Error(\"Initial Vector is undefined or missing.\");\n}\n\nconst ivData = initialVector.data || initialVector;\nif (!ivData) {\n throw new Error(\"Initial Vector 'data' is undefined or missing.\");\n}\n\nlet ivBuffer;\nif (typeof ivData === \"string\") {\n ivBuffer = Buffer.from(ivData, 'base64');\n} else if (Buffer.isBuffer(ivData)) {\n ivBuffer = ivData;\n} else if (Array.isArray(ivData)) {\n ivBuffer = Buffer.from(ivData);\n} else {\n throw new Error(\"Initial Vector 'data' is in an unsupported format.\");\n}\n\nconst invertedIV = Buffer.from(ivBuffer.map((b) => ~b & 0xFF));\n\n// Access AES Key from the correct path\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) {\n throw new Error(\"AES Key is missing.\");\n}\nconst aesKey = Buffer.from(aesKeyBase64, \"base64\");\n\n// Define the response data with the extracted seats\nconst responseData = {\n status: \"active\",\n seats: seats.map((seat, index) => ({\n id: `seat_${index + 1}`,\n title: seat\n }))\n};\n\n// Define the flow_token\nconst flowToken = $('Json Parser').first().json.flow_token || \"\";\nif (!flowToken) {\n throw new Error(\"Flow token is missing.\");\n}\n\nconst nextScreen = \"SUMMARY\";\n\nconst responseMessage = JSON.stringify({\n version: \"3.0\",\n action: \"data_exchange\",\n screen: nextScreen,\n data: responseData,\n flow_token: flowToken,\n});\n\n// Encrypt Response using AES-GCM\nconst cipher = crypto.createCipheriv(\"aes-128-gcm\", aesKey, invertedIV);\nlet encryptedResponse = Buffer.concat([\n cipher.update(responseMessage, \"utf-8\"),\n cipher.final()\n]);\n\nconst authTag = cipher.getAuthTag();\nconst result = Buffer.concat([encryptedResponse, authTag]);\nconst base64Response = result.toString(\"base64\");\n\n// Return the encrypted response\nreturn [{ body: base64Response }];\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},View on GitHub (pinned to 94007c1445)
Solutions
- Log the parser output: console.log(JSON.stringify($('Json Parser').first().json)) and locate flow_token's actual path.
- Read flow_token from the decrypted payload path if that is where it lives: JSON.parse($('Decryption Code').first().json.decryptedPayload).flow_token.
- Ensure test requests use a genuine WhatsApp Flows endpoint hit so flow_token is present; reject non-Flows traffic before this node.
- Keep field name exactly flow_token (snake_case) - WhatsApp sends it lowercase.
Example fix
// before
const flowToken = $('Json Parser').first().json.flow_token || "";
if (!flowToken) { throw new Error("Flow token is missing."); }
// after
const parsed = $('Json Parser').first().json;
const flowToken = parsed.flow_token ?? parsed.decrypted_payload?.flow_token;
if (typeof flowToken !== 'string' || flowToken.length === 0) {
throw new Error("Flow token is missing - payload was not a valid Flows data_exchange request");
} Defensive patterns
Strategy: validation
Validate before calling
const parsed = $('Json Parser').first().json;
const flowToken = parsed.flow_token ?? parsed.decrypted_payload?.flow_token;
if (typeof flowToken !== 'string' || flowToken.length === 0) {
throw new Error('Flow token missing - not a valid Flows data_exchange request');
} Type guard
const hasFlowToken = (j) => typeof j?.flow_token === 'string' && j.flow_token.length > 0;
Prevention
- Read flow_token from the decrypted payload, the authoritative source
- Gate encrypt branches on the decrypted action/screen so non-Flows traffic never reaches them
- Log the parsed payload keys once to lock in the schema
- Use real WhatsApp Flows requests for testing, not bare POST bodies
When it happens
Trigger: The inbound decrypted payload did not contain flow_token (e.g. a resume/ack message or a malformed test payload); the 'Json Parser' node ran before decryption populated its output, so `.first().json` lacks the key; the decrypted JSON nests flow_token at a different depth than the parser exposes.
Common situations: Sending a test webhook body without a real WhatsApp Flows encrypted payload; flow_token located under decryptedPayload.flow_token but read from the parsed top level; changes in the WhatsApp Flows protocol payload shape; node renamed so the Json Parser reference resolves to nothing.
Related errors
- Initial Vector 'data' is undefined or missing.
- Initial Vector 'data' is in an unsupported format.
- AES Key is missing.
- Seats data is missing or invalid.
- Insufficient stock for ${$('Retrieve Issue Request Details')
AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15).
Data as JSON: /api/errors/533c6db6cd3b6fa0.
Report an issue: GitHub.