Skip to main content

Polar retries for days. Five minutes is the wrong clock.

00:01:11:73

Polar retries webhooks for days, and it keeps the original timestamp. A 5-minute skew check treats those retries as forged. They aren't.

ts
export const POLAR_WEBHOOK_TOLERANCE_SECONDS = 7 * 24 * 60 * 60;

That's a week. Replay inside the window is not "accept the body twice." I lease the event on (provider, providerEventId) before applying it.

ts
const lease = await this.webhookEvents.beginProcessing({
  provider: paymentProvider.providerId,
  providerEventId,
  eventType: parsed.providerEventType,
  payload: envelope,
});

Null lease means applied, ignored, poisoned, or still in flight. In flight returns 503 so Polar tries again. Already applied returns 200 and stops.

Polar signs the same secret two different ways

Old dashboard secrets HMAC the UTF-8 bytes of the full string, including whsec_. New secrets follow Standard Webhooks: strip the prefix, base64-decode the rest. Polar flipped that for secrets created after the cutoff. Older ones still sign the old way.

ts
export function polarWebhookHmacKeys(secret: string): Buffer[] {
  const polarSdk = polarWebhookHmacKey(secret, 'polar-sdk');
  const standard = polarWebhookHmacKey(secret, 'standard-webhooks');
  if (standard.length === 0 || standard.equals(polarSdk)) {
    return [polarSdk];
  }
  return [polarSdk, standard];
}

I try both keys with timingSafeEqual. One secret, two encodings. Pick only the scheme from last week's Polar docs and half your environments 403.

Widen the clock for retries. Narrow the duplicate with an idempotency row. The encoding of the secret is not a constant.