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

  1. Log the parser output: console.log(JSON.stringify($('Json Parser').first().json)) and locate flow_token's actual path.
  2. Read flow_token from the decrypted payload path if that is where it lives: JSON.parse($('Decryption Code').first().json.decryptedPayload).flow_token.
  3. Ensure test requests use a genuine WhatsApp Flows endpoint hit so flow_token is present; reject non-Flows traffic before this node.
  4. 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

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


AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15). Data as JSON: /api/errors/533c6db6cd3b6fa0. Report an issue: GitHub.