{"record":{"id":"201e770dcd9c4092","repo":"twentyhq/twenty","slug":"invalid-webhook-signature-signaturecheck-error","errorCode":null,"errorMessage":"Invalid webhook signature: ${signatureCheck.error}","messagePattern":"Invalid webhook signature: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/twenty-apps/public/call-recorder/src/logic-functions/recall-webhook.ts","lineNumber":48,"sourceCode":"    );\n  }\n\n  const { rawBody } = routePayload;\n\n  if (isUndefined(rawBody)) {\n    throw new Error(\n      'Raw request body was not forwarded by the server; cannot verify the webhook signature',\n    );\n  }\n\n  const signatureCheck = verifyRecallWebhookSignature({\n    rawBody,\n    headers: routePayload.headers,\n    secret: webhookSecret,\n  });\n\n  if (!signatureCheck.valid) {\n    throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);\n  }\n\n  const body = routePayload.body;\n\n  if (isUndefined(body) || isNull(body)) {\n    throw new Error('Webhook payload was empty');\n  }\n\n  const workspaceId = extractTwentyWorkspaceIdFromRecallWebhook(body);\n\n  if (!isNonEmptyString(workspaceId)) {\n    throw new Error(\n      'Webhook payload is missing the Twenty workspace id in the Recall bot metadata',\n    );\n  }\n\n  return {\n    workspaceId,","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/twentyhq/twenty/blob/1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-webhook.ts#L30-L66","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst signatureCheck = verifyRecallWebhookSignature({ rawBody, headers: routePayload.headers, secret: webhookSecret });\nif (!signatureCheck.valid) {\n  throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);\n}\n\n// after — log the diagnostic context so the cause is identifiable in logs\nif (!signatureCheck.valid) {\n  console.error(\n    '[recall-webhook] signature invalid reason=', signatureCheck.error,\n    'hasRawBody=', !isUndefined(rawBody),\n    'forwardedSigHeaders=', ['webhook-signature','svix-signature'].filter((h) => isNonEmptyString(routePayload.headers?.[h] as string | undefined)),\n  );\n  throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);\n}","handlingStrategy":"try-catch","validationCode":"// Defensive pre-check of forwarded signature headers.\nconst sigHeaders = ['webhook-signature', 'svix-signature'];\nconst hasSig = sigHeaders.some((h) => isNonEmptyString(routePayload.headers?.[h] as string | undefined));\nif (!hasSig) {\n  throw new Error('Cannot verify webhook: no signature header was forwarded');\n}","typeGuard":null,"tryCatchPattern":"const signatureCheck = verifyRecallWebhookSignature({ rawBody, headers: routePayload.headers, secret: webhookSecret });\nif (!signatureCheck.valid) {\n  // Stale timestamps from Svix retry backlog are not security failures; log and let Svix retry.\n  console.error('[recall-webhook] signature invalid:', signatureCheck.error);\n  throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);\n}","preventionTips":["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."],"tags":["webhook","recall","call-recorder","security","signatures","secrets"],"backgroundTag":null,"analyzedSha":"1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6","analyzedAt":"2026-08-12T15:37:27.593Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}