I want to clarify the real issue here, because in my opinion this is not about whether the HTTP Request node can send header, that part works fine.
The problem is how the Wait node validates authentication on resume.
The real issue
When using a Wait node with “Resume via Webhook” and incomingAuthentication = headerAuth:
-
The Wait node only supports static header values
-
It does NOT evaluate expressions per execution
-
Dynamic secrets (like a per-run x-api-key) will never match
-
Result: the workflow stays pending forever
This explains the behavior exactly:
| Scenario |
Result |
Static x-api-key |
resumes |
Dynamic x-api-key |
pending forever |
This is by design, not a bug.
Suggested solution
- Set the Wait node authentication to
None
- Resume the workflow normally
- Immediately validate the dynamic key yourself in a Code node
The resume URL is already a high-entropy, execution-scoped secret.
The header is an additional guard, but must be validated manually.
Here’s an example. Please note this is for proof of concept only as I have no way of testing this beyond syntax:
Place this node immediately after the Wait node
Mode: Run once for each item
// ============================================================================
// CALLBACK AUTH VALIDATION
// MODE: Run once for each item
// PURPOSE:
// - Validate dynamic x-api-key returned via Wait webhook callback
// - Enforce per-execution auth (Wait node auth is static-only)
// ============================================================================
// -------------------- INPUT --------------------
const item = $json;
// Webhook resume metadata (shape varies slightly by version)
const headers =
item.headers ||
item.webhookHeaders ||
item.request?.headers ||
{};
// Case-insensitive header lookup
function getHeader(headers, name) {
const key = Object.keys(headers).find(
k => k.toLowerCase() === name.toLowerCase()
);
return key ? headers[key] : undefined;
}
// -------------------- EXPECTED KEY --------------------
const expectedKey = item.xApiKey;
if (!expectedKey) {
throw new Error(“Missing expected xApiKey in execution context.”);
}
// -------------------- RECEIVED KEY --------------------
const receivedKey = getHeader(headers, “x-api-key”);
if (!receivedKey) {
throw new Error(“Missing x-api-key header in callback request.”);
}
// -------------------- VALIDATION --------------------
if (receivedKey !== expectedKey) {
throw new Error(“Callback authentication failed: x-api-key mismatch.”);
}
// -------------------- SUCCESS --------------------
return item;
Why this should work:
- Wait node auth is static only
- Expressions are not evaluated at resume time
- This pattern gives you:
- per-execution secrets
- deterministic behavior
- full control over failure handling
Alternatives
- Put the key in the callback URL as a query param
- Use a single static shared secret (if acceptable)
- Use manual validation + logging instead of throwing
If anyone has found a cleaner native approach, I’d genuinely like to learn, but as of now, this is the only reliable pattern I know of.