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— thewebhook-idheader, the spec’s canonical idempotency key.event.type— the body’stype.event.timestamp— the signedwebhook-timestampheader. The body’s owntimestampis an ISO string of when the event occurred, which can be earlier than the delivery.event.payload—{ type, timestamp, data };datais the full resource — a checkout, order, subscription — identified bypayload.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.