ComposioHQ/composio · error · ComposioWebhookSignatureVerificationError

The webhook timestamp is outside the allowed tolerance. The

Error message

The webhook timestamp is outside the allowed tolerance. The webhook was sent ${Math.round(timeDifference / 1000)} seconds ago, but the maximum allowed age is ${tolerance} seconds.

What it means

The webhook's Unix timestamp differs from the server clock by more than the configured tolerance (in seconds; the comparison is tolerance*1000 ms). This is a replay-attack mitigation: stale or far-future signatures are rejected even if the HMAC itself is valid.

Source

Thrown at ts/packages/core/src/models/Triggers.ts:1314

  /**
   * Validates that the webhook timestamp is within the allowed tolerance
   * @private
   */
  private validateWebhookTimestamp(webhookTimestamp: string, tolerance: number): void {
    const timestampSeconds = parseInt(webhookTimestamp, 10);

    if (Number.isNaN(timestampSeconds)) {
      throw new ComposioWebhookPayloadError(
        `Invalid webhook timestamp: ${webhookTimestamp}. Expected Unix timestamp in seconds.`
      );
    }

    const webhookTimeMs = timestampSeconds * 1000;
    const currentTime = Date.now();
    const timeDifference = Math.abs(currentTime - webhookTimeMs);

    if (timeDifference > tolerance * 1000) {
      throw new ComposioWebhookSignatureVerificationError(
        `The webhook timestamp is outside the allowed tolerance. ` +
          `The webhook was sent ${Math.round(timeDifference / 1000)} seconds ago, ` +
          `but the maximum allowed age is ${tolerance} seconds.`
      );
    }
  }
}

View on GitHub (pinned to 64b1b85502)

Solutions

  1. If replaying captured requests in dev, refresh the timestamp/signature or raise the tolerance explicitly
  2. Check server clock sync (NTP) if genuine deliveries are being rejected
  3. Pass an explicit, sensible tolerance (seconds) when calling verification
  4. Process webhooks promptly instead of enqueueing raw headers for much later verification

Example fix

// before
verifyWebhookSignature(payload, sig, { secret, webhookId, webhookTimestamp }); // default tolerance too small for delayed replay
// after
verifyWebhookSignature(payload, sig, {
  secret,
  webhookId,
  webhookTimestamp,
  tolerance: 300, // seconds
});
Defensive patterns

Strategy: validation

Validate before calling

const ts = Number(req.headers['webhook-timestamp']);
const age = Math.abs(Date.now() / 1000 - ts);
if (age > TOLERANCE_SECONDS) return res.status(400).send('Webhook too old');

Type guard

null

Try / catch

try { verifyWebhookSignature(..., { tolerance: 300 }); } catch (e) { if (e instanceof ComposioWebhookSignatureVerificationError && /tolerance/.test(e.message)) return res.status(400).end(); throw e; }

Prevention

When it happens

Trigger: Replaying an old (captured) webhook request, verifying a recorded webhook during development after delay exceeds tolerance, a server clock skewed by more than the tolerance, or a tolerance set too small (e.g. 0/undefined coerced to a tiny value).

Common situations: Replaying captured curl requests in debugging, queued/delayed webhook processing, NTP drift on the host, or copying test fixtures with hardcoded old timestamps.

Related errors


AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28). Data as JSON: /api/errors/5f716b6f72c23e8b. Report an issue: GitHub.