Skip to content
Webhooks SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Polar

Verify and handle Polar webhooks — checkout, order, subscription, customer, and benefit events.

Create the endpoint under Settings → Webhooks in your Polar organization, pass its secret, and handle events by name:

import { createWebhookHandler } from 'webhooks-sdk'
import { polar } from 'webhooks-sdk/polar'

const handler = createWebhookHandler({
  provider: polar({ secret: process.env.POLAR_WEBHOOK_SECRET! }),
  on: {
    'order.paid': async (event) => {
      await fulfill(event.payload.data.id)
    },
    'subscription.revoked': async (event) => {
      await revokeAccess(event.payload.data.id)
    },
  },
})

export const POST = handler.fetch

The secret is the value you set — or generate — when creating the endpoint. Pass it exactly as the dashboard shows it; see the secret is raw below. Polar signs with Standard Webhooks over the spec’s webhook-* headers — the id and timestamp are inside the signed material, so a replayed request is rejected on its own.

Options

Option Type Default
secret string | string[] — The endpoint’s secret, verbatim from the dashboard. Pass an array during rotation.
tolerance number 300 Replay window in seconds.

Events

checkout.created · checkout.updated · checkout.expired · customer.created · customer.updated · customer.deleted · customer.state_changed · subscription.created · subscription.updated · subscription.active · subscription.canceled · subscription.uncanceled · subscription.revoked · subscription.cycled · subscription.past_due · subscription.paused · subscription.resumed · order.created · order.updated · order.paid · order.refunded · refund.created · refund.updated · benefit.created · benefit.updated · benefit_grant.created · benefit_grant.updated · benefit_grant.revoked · product.created · product.updated · discount.created · discount.updated · discount.deleted · organization.updated

These autocomplete in the on map; any other string still routes, so an event type Polar adds later is handled without an SDK update.

The secret is raw, not whsec_

Unlike most Standard Webhooks vendors, Polar’s endpoint secret is a plain string — no whsec_ prefix, no base64. The spec keys on base64-decoded bytes, so the wrapper base64-encodes the secret for you, the same bridge Polar’s own SDK makes. Pass the dashboard value verbatim; encoding it yourself makes every delivery fail as a bad signature. The generic standardWebhooks({ id: 'polar' }) provider does not make this bridge — use the wrapper.

Pick the Raw payload format

An endpoint can deliver in Raw, Discord, or Slack format — and pasting a Discord or Slack webhook URL switches the format automatically. Only Raw is the JSON envelope this provider parses; the other two are for posting notifications into chat channels, not for verification.

The envelope

  • event.id — the webhook-id header, the spec’s canonical idempotency key.
  • event.type — the body’s type.
  • event.timestamp — the signed webhook-timestamp header. The body’s own timestamp is an ISO string of when the event occurred, which can be earlier than the delivery.
  • event.payload — { type, timestamp, data }; data is the full resource — a checkout, order, subscription — identified by payload.data.id. No re-fetch needed.

Standalone & testing

The wrapper is the Standard Webhooks provider with id: 'polar', so the standalone triple lives there:

import {
  verifyStandardWebhook,  // (raw, { secret }) — throws on failure
  parseStandardWebhook,   // (raw) — the envelope
  signStandardWebhook,    // (body, secret) — all three webhook-* headers, for tests
} from 'webhooks-sdk/standard-webhooks'

The standalone helpers implement the spec, so they expect the base64 form the wrapper produces internally — hand them Buffer.from(secret).toString('base64') to match Polar’s wire signatures. See Testing.

Last updated on September 1, 2026

Was this page helpful?