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

  1. Read `signatureCheck.error` — it distinguishes missing-header, stale-timestamp, and bad-signature causes; act on that specific reason.
  2. If the secret was rotated, copy the current Recall signing secret into `RECALL_WEBHOOK_SECRET` (server scope) on both sides.
  3. 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.
  4. If the error is a stale timestamp, investigate queue backlog / clock skew rather than treating it as a bad signature.
  5. 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

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


AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12). Data as JSON: /api/errors/201e770dcd9c4092. Report an issue: GitHub.