{"record":{"id":"533c6db6cd3b6fa0","repo":"Zie619/n8n-workflows","slug":"flow-token-is-missing","errorCode":null,"errorMessage":"Flow token is missing.","messagePattern":"Flow token is missing\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json","lineNumber":220,"sourceCode":"        -1320,\n        860\n      ],\n      \"parameters\": {\n        \"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\"\n      },\n      \"typeVersion\": 2,\n      \"notes\": \"This code node performs automated tasks as part of the workflow.\"\n    },\n    {\n      \"id\": \"17c055f3-c278-48c4-89d4-d305a35bc526\",\n      \"name\": \"Encrypt Return\",\n      \"type\": \"n8n-nodes-base.code\",\n      \"position\": [\n        -200,\n        760\n      ],\n      \"parameters\": {\n        \"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\"\n      },\n      \"typeVersion\": 2,\n      \"notes\": \"This code node performs automated tasks as part of the workflow.\"\n    },\n    {\n      \"id\": \"412f55e3-5867-4e65-a494-3e3bf991d59c\",\n      \"name\": \"Encrypt Return1\",\n      \"type\": \"n8n-nodes-base.code\",\n      \"position\": [\n        -200,\n        1000\n      ],\n      \"parameters\": {\n        \"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\"\n      },\n      \"typeVersion\": 2,\n      \"notes\": \"This code node performs automated tasks as part of the workflow.\"\n    },","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/Zie619/n8n-workflows/blob/94007c1445d9258a7da116646b79473e7c7c3282/workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json#L202-L238","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst flowToken = $('Json Parser').first().json.flow_token || \"\";\nif (!flowToken) { throw new Error(\"Flow token is missing.\"); }\n\n// after\nconst parsed = $('Json Parser').first().json;\nconst flowToken = parsed.flow_token ?? parsed.decrypted_payload?.flow_token;\nif (typeof flowToken !== 'string' || flowToken.length === 0) {\n  throw new Error(\"Flow token is missing - payload was not a valid Flows data_exchange request\");\n}","handlingStrategy":"validation","validationCode":"const parsed = $('Json Parser').first().json;\nconst flowToken = parsed.flow_token ?? parsed.decrypted_payload?.flow_token;\nif (typeof flowToken !== 'string' || flowToken.length === 0) {\n  throw new Error('Flow token missing - not a valid Flows data_exchange request');\n}","typeGuard":"const hasFlowToken = (j) =>\n  typeof j?.flow_token === 'string' && j.flow_token.length > 0;","tryCatchPattern":null,"preventionTips":["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"],"tags":["n8n","whatsapp-flows","flow-token","validation"],"backgroundTag":null,"analyzedSha":"94007c1445d9258a7da116646b79473e7c7c3282","analyzedAt":"2026-08-15T04:10:37.591Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}