twentyhq/twenty · error · Error
Invalid webhook signature: ${signatureCheck.error}
Error message
Invalid webhook signature: ${signatureCheck.error} What it means
The handler runs `verifyRecallWebhookSignature` over the raw body, the forwarded `webhook-*`/`svix-*` headers, and the `RECALL_WEBHOOK_SECRET`. If verification fails it returns `{ valid: false, error }` and the handler throws an error embedding that reason. A signature failure means the payload was tampered with, the secret is wrong, the timestamp is outside the replay window, or the required headers are missing.
Source
Thrown at packages/twenty-apps/public/call-recorder/src/logic-functions/recall-webhook.ts:48
);
}
const { rawBody } = routePayload;
if (isUndefined(rawBody)) {
throw new Error(
'Raw request body was not forwarded by the server; cannot verify the webhook signature',
);
}
const signatureCheck = verifyRecallWebhookSignature({
rawBody,
headers: routePayload.headers,
secret: webhookSecret,
});
if (!signatureCheck.valid) {
throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
}
const body = routePayload.body;
if (isUndefined(body) || isNull(body)) {
throw new Error('Webhook payload was empty');
}
const workspaceId = extractTwentyWorkspaceIdFromRecallWebhook(body);
if (!isNonEmptyString(workspaceId)) {
throw new Error(
'Webhook payload is missing the Twenty workspace id in the Recall bot metadata',
);
}
return {
workspaceId,View on GitHub (pinned to 1f5dd2bbd2)
Solutions
- Read `signatureCheck.error` — it distinguishes missing-header, stale-timestamp, and bad-signature causes; act on that specific reason.
- If the secret was rotated, copy the current Recall signing secret into `RECALL_WEBHOOK_SECRET` (server scope) on both sides.
- Confirm the `forwardedRequestHeaders` list (`webhook-id`, `webhook-timestamp`, `webhook-signature`, `svix-id`, `svix-timestamp`, `svix-signature`) is actually forwarded by the platform to the route.
- If the error is a stale timestamp, investigate queue backlog / clock skew rather than treating it as a bad signature.
- Ensure no intermediary re-encodes the raw body (whitespace, ordering) between Recall and the verifier.
Example fix
// before
const signatureCheck = verifyRecallWebhookSignature({ rawBody, headers: routePayload.headers, secret: webhookSecret });
if (!signatureCheck.valid) {
throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
}
// after — log the diagnostic context so the cause is identifiable in logs
if (!signatureCheck.valid) {
console.error(
'[recall-webhook] signature invalid reason=', signatureCheck.error,
'hasRawBody=', !isUndefined(rawBody),
'forwardedSigHeaders=', ['webhook-signature','svix-signature'].filter((h) => isNonEmptyString(routePayload.headers?.[h] as string | undefined)),
);
throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
} Defensive patterns
Strategy: try-catch
Validate before calling
// Defensive pre-check of forwarded signature headers.
const sigHeaders = ['webhook-signature', 'svix-signature'];
const hasSig = sigHeaders.some((h) => isNonEmptyString(routePayload.headers?.[h] as string | undefined));
if (!hasSig) {
throw new Error('Cannot verify webhook: no signature header was forwarded');
} Try / catch
const signatureCheck = verifyRecallWebhookSignature({ rawBody, headers: routePayload.headers, secret: webhookSecret });
if (!signatureCheck.valid) {
// Stale timestamps from Svix retry backlog are not security failures; log and let Svix retry.
console.error('[recall-webhook] signature invalid:', signatureCheck.error);
throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
} Prevention
- Rotate the Recall signing secret on both Recall and the app variable simultaneously.
- Ensure the declared `forwardedRequestHeaders` (webhook-*/svix-*) are actually forwarded by the platform.
- Distinguish 'stale timestamp' from 'bad signature' in `signatureCheck.error` before alarming.
- Prevent any intermediary from re-encoding the raw body so the HMAC input matches.
When it happens
Trigger: The `RECALL_WEBHOOK_SECRET` does not match the secret Recall.ai signs with (rotated on one side only); the webhook-id/timestamp/signature headers were not forwarded so verification cannot run; the timestamp is stale (replay-protection window exceeded, often due to queue backlog or clock skew); or the body was re-encoded between Recall and the verifier so the HMAC input differs.
Common situations: Secret rotation done on Recall but not in the app variable (or vice-versa); a platform change that stopped forwarding the `svix-*`/`webhook-*` headers declared in `forwardedRequestHeaders`; Svix retries arriving after a long delay so the timestamp falls outside the validity window; an intermediary re-serializing the JSON body.
Related errors
- RECALL_WEBHOOK_SECRET server variable is not set. A server a
- Raw request body was not forwarded by the server; cannot ver
- failed to request artifact import for call recording ${callR
- download failed with status ${response.status}
- download response is missing content-length
AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12).
Data as JSON: /api/errors/201e770dcd9c4092.
Report an issue: GitHub.