Guide to Polar Webhooks Features and Best Practices
Polar has become the go-to billing and monetization platform for developers and digital creators who want to sell software, subscriptions, and digital products without the complexity of traditional payment infrastructure. Beyond checkout and subscription management, Polar's webhook system enables developers to react to billing events in real-time, powering everything from access provisioning to usage tracking and customer lifecycle automation.
This guide covers everything you need to know about Polar webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.
What are Polar webhooks?
Polar webhooks are HTTP callbacks that deliver event notifications to your endpoints whenever something significant happens in your Polar organization. When a customer makes a purchase, a subscription renews, a benefit is granted, or a refund is issued, Polar sends a signed JSON payload to URLs you configure. This enables you to synchronize your application state, provision access, trigger downstream workflows, and integrate with any service that can receive HTTP requests.
Polar's webhook implementation follows the Standard Webhooks specification, an open standard for webhook delivery that provides consistent signing, verification, and retry semantics across platforms.
Polar webhook features
| Feature | Details |
|---|---|
| Webhook configuration | Polar dashboard UI or API |
| Signing standard | Standard Webhooks (HMAC-based) |
| Delivery timeout | 10 seconds (recommended: respond within 2 seconds) |
| Retry logic | Up to 10 retries with exponential backoff |
| Delivery formats | Raw JSON, Slack, Discord |
| Manual retry | Available via delivery log in dashboard |
| Browsable log | Delivery history with payload inspection per endpoint |
| Endpoint auto-disable | After 10 consecutive failed deliveries |
| SDK support | TypeScript, Python, Go, Ruby, PHP (beta) |
| Sandbox environment | Full sandbox for testing without real charges |
| IP allowlisting | Published production and sandbox IPs |
| Rate limit | 100 requests per second per IP |
Supported webhook events
Polar provides over 25 webhook events organized into logical categories covering the entire billing and customer lifecycle.
Billing events
| Event | Description |
|---|---|
checkout.created | A new checkout session has been created |
checkout.updated | A checkout session has been updated |
Customer events
| Event | Description |
|---|---|
customer.created | A new customer has been created |
customer.updated | A customer has been updated |
customer.deleted | A customer has been deleted |
customer.state_changed | A customer's state has changed, including active subscriptions and granted benefits |
Subscription events
| Event | Description |
|---|---|
subscription.created | A new subscription has been created |
subscription.active | A subscription has become active |
subscription.updated | Catch-all for cancellations, un-cancellations, and other changes |
subscription.canceled | A subscription has been canceled |
subscription.uncanceled | A subscription cancellation has been reversed |
subscription.past_due | A subscription payment has failed; customer can recover by updating payment method |
subscription.revoked | A subscription has been definitively revoked (billing stopped, benefits revoked) |
Order events
| Event | Description |
|---|---|
order.created | A new order has been created. Use billing_reason to distinguish: purchase, subscription_create, subscription_cycle, subscription_update |
order.paid | An order payment has been successfully processed |
order.updated | An order has been updated |
order.refunded | An order has been refunded |
Refund events
| Event | Description |
|---|---|
refund.created | A new refund has been created |
refund.updated | A refund has been updated |
Benefit grant events
| Event | Description |
|---|---|
benefit_grant.created | A benefit has been granted to a customer |
benefit_grant.updated | A benefit grant has been updated |
benefit_grant.revoked | A benefit grant has been revoked |
Organization events
| Event | Description |
|---|---|
benefit.created | A new benefit type has been created |
benefit.updated | A benefit type has been updated |
product.created | A new product has been created |
product.updated | A product has been updated |
organization.updated | The organization settings have been updated |
Subscription lifecycle sequences
Understanding how webhook events relate to each other during subscription lifecycle changes is critical for building reliable integrations.
End-of-period cancellation (default)
When a subscription is canceled by the customer or merchant, these events fire immediately:
subscription.updatedsubscription.canceled
The subscription retains active status with cancel_at_period_end set to true. When the billing period ends:
subscription.updatedsubscription.revoked
The subscription now has canceled status. Billing stops and benefits are revoked.
Immediate revocation
When a merchant cancels with immediate revocation, all three events fire at once:
subscription.updatedsubscription.canceledsubscription.revoked
Renewal sequence
When a subscription renews:
subscription.updated— reflects the newcurrent_period_startandcurrent_period_endorder.created— the invoice for the new cycle (status:pending)order.updated— once payment processes (status:paid)order.paid— confirmation of successful payment
Setting up Polar webhooks
Via the Polar dashboard
Navigate to your organization's Settings page
Click the Add Endpoint button
Enter the URL where webhook events should be sent
Choose a delivery format:
- Raw (default) — standard JSON payload for custom integrations
- Discord — automatically formatted for Discord channel webhooks
- Slack — automatically formatted for Slack incoming webhooks
Polar auto-detects Discord and Slack URLs and selects the appropriate format.
Set your secret — either provide your own or let Polar generate a random one. This is used to cryptographically sign every delivery.
Subscribe to events — select which events you want to receive
Webhook payload structure
Polar webhook payloads follow a consistent structure. Each delivery includes Standard Webhooks headers for verification:
webhook-id— unique identifier for the webhook deliverywebhook-timestamp— Unix timestamp of when the webhook was sentwebhook-signature— HMAC signature for verification
The body contains a JSON payload with the event type and relevant data. Here's an example subscription.created payload:
{
"type": "subscription.created",
"data": {
"created_at": "2025-06-15T10:30:00.000Z",
"modified_at": "2025-06-15T10:30:00.000Z",
"id": "sub_1234567890",
"status": "active",
"current_period_start": "2025-06-15T10:30:00.000Z",
"current_period_end": "2025-07-15T10:30:00.000Z",
"cancel_at_period_end": false,
"customer_id": "cust_abc123",
"product_id": "prod_xyz789",
"price_id": "price_def456"
}
}
Best practices when working with Polar webhooks
Verifying webhook signatures
Polar cryptographically signs every webhook delivery using the Standard Webhooks specification. Always verify signatures to ensure payloads genuinely originated from Polar.
Using the TypeScript SDK (recommended):
import express, { Request, Response } from 'express';
import {
validateEvent,
WebhookVerificationError,
} from '@polar-sh/sdk/webhooks';
const app = express();
app.post(
'/webhook',
express.raw({ type: 'application/json' }),
(req: Request, res: Response) => {
try {
const event = validateEvent(
req.body,
req.headers,
process.env['POLAR_WEBHOOK_SECRET'] ?? '',
);
// Process the event based on type
switch (event.type) {
case 'subscription.created':
// Handle new subscription
break;
case 'subscription.revoked':
// Revoke access
break;
case 'order.paid':
// Fulfill order
break;
}
res.status(202).send('');
} catch (error) {
if (error instanceof WebhookVerificationError) {
res.status(403).send('');
}
throw error;
}
},
);
Using the Python SDK:
import os
from flask import Flask, request
from polar_sdk.webhooks import validate_event, WebhookVerificationError
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
try:
event = validate_event(
payload=request.data,
headers=dict(request.headers),
secret=os.environ['POLAR_WEBHOOK_SECRET'],
)
match event.type:
case 'subscription.created':
# Handle new subscription
pass
case 'subscription.revoked':
# Revoke access
pass
case 'order.paid':
# Fulfill order
pass
return '', 202
except WebhookVerificationError:
return '', 403
Custom verification (without SDK):
If you're not using an official SDK, remember that the webhook secret must be base64 encoded before generating the signature. The Standard Webhooks specification provides libraries in many languages for this purpose. The SDKs handle encoding automatically.
Respond quickly and process asynchronously
Polar times out webhook deliveries after 10 seconds, and recommends your endpoint respond within 2 seconds. Always acknowledge the webhook immediately and defer heavy processing to a background job.
Implement idempotent processing
Webhooks may be delivered more than once due to retries or network issues. Use the webhook-id header or a combination of event type and resource ID to ensure you don't process the same event twice:
async function processWebhook(event, webhookId) {
const idempotencyKey = `polar:${webhookId}`;
const alreadyProcessed = await redis.get(`processed:${idempotencyKey}`);
if (alreadyProcessed) {
console.log(`Webhook ${webhookId} already processed, skipping`);
return;
}
await handleEvent(event);
// Mark as processed with a 24-hour TTL
await redis.setex(`processed:${idempotencyKey}`, 86400, '1');
}
Use customer.state_changed for access control
Rather than listening to individual subscription and benefit events, the customer.state_changed event provides a consolidated view of a customer's active subscriptions and granted benefits. This simplifies access control logic:
case 'customer.state_changed':
const { customer_id, active_subscriptions, granted_benefits } = event.data;
await syncCustomerEntitlements(customer_id, active_subscriptions, granted_benefits);
break;
Handle subscription cancellation sequences correctly
Polar distinguishes between cancellation (intent to cancel) and revocation (access actually removed). Don't revoke access on subscription.canceled — wait for subscription.revoked.
Detect subscription renewals via order.created
Polar does not currently have a dedicated subscription.renewed event. To detect renewals, listen for order.created and check the billing_reason field.
Polar webhook limitations and pain points
Tight 10-second delivery timeout
The Problem: Polar times out webhook deliveries after 10 seconds. The documentation recommends responding within 2 seconds and warns the timeout may be lowered further in the future. Endpoints that perform any synchronous processing (database writes, third-party API calls, or complex validation) risk exceeding this window.
Why It Happens: Polar optimizes for fast delivery throughput across their infrastructure. A short timeout prevents slow endpoints from creating back-pressure that would delay deliveries to other customers.
Workarounds:
- Acknowledge webhooks immediately with a
202response and queue payloads for asynchronous processing via a background worker - Avoid any blocking I/O in your webhook handler before sending the response
- Use a message queue (Hookdeck, Redis, SQS, RabbitMQ) between your endpoint and your processing logic
How Hookdeck Can Help: Hookdeck accepts webhook deliveries on your behalf and provides configurable delivery timeouts, giving your endpoint additional time to process without risking failed deliveries from Polar. Hookdeck's built-in queuing ensures reliable delivery even when your endpoint is temporarily slow.
Automatic endpoint disabling after consecutive failures
The Problem: Polar automatically disables your webhook endpoint after 10 consecutive failed deliveries (any non-2xx response). Once disabled, the endpoint silently stops receiving all events. An email notification is sent to the organization admin, but events that occur while the endpoint is disabled are lost.
Why It Happens: This is a protective mechanism to avoid wasting resources delivering webhooks to endpoints that are consistently failing, which could indicate a misconfigured or decommissioned URL.
Workarounds:
- Implement robust error handling in your webhook endpoint to always return a
2xxstatus, even if downstream processing fails (acknowledge first, process later) - Monitor the email address associated with your Polar organization for disabling notifications
- Periodically check your webhook endpoint status in the Polar dashboard
- After fixing the issue, manually re-enable the endpoint in your organization's webhook settings
How Hookdeck Can Help: Hookdeck acts as a stable intermediary that always accepts deliveries from Polar, preventing your endpoint from being disabled. If your downstream service has issues, Hookdeck queues events and retries delivery with configurable backoff, so you never lose events due to temporary outages.
No dead letter queue for exhausted retries
The Problem: When all 10 retry attempts are exhausted, or when an endpoint is disabled, webhook events are effectively lost. Polar does not maintain a dead letter queue that preserves failed deliveries for later inspection or replay.
Why It Happens: Polar's delivery log allows you to view past deliveries and manually re-trigger them from the dashboard, which partially addresses this need. However, there is no automated mechanism to hold failed events for bulk replay or programmatic recovery.
Workarounds:
- Use the Polar dashboard's delivery history to manually inspect and re-trigger failed deliveries
- Implement your own logging at the webhook handler level to capture every incoming payload before processing
- Build a reconciliation process that periodically queries the Polar API to check for any events your system may have missed
How Hookdeck Can Help: Hookdeck automatically preserves all failed webhook deliveries in a dead letter queue. You can inspect, debug, and replay them individually or in bulk once issues are resolved, ensuring no billing events are permanently lost.
No custom header support on outbound deliveries
The Problem: Polar webhook deliveries include Standard Webhooks headers for signing (webhook-id, webhook-timestamp, webhook-signature) but do not support adding arbitrary custom headers. You cannot configure headers like X-API-Key, X-Tenant-ID, or custom Authorization schemes.
Why It Happens: Polar's webhook system focuses on the Standard Webhooks specification, which defines a specific set of headers for signing and verification. Custom header support is outside this specification's scope.
Workarounds:
- Use URL path segments or query parameters to encode tenant or routing information (e.g.,
https://your-api.com/webhooks/polar/tenant-123) - Deploy a lightweight proxy that adds required headers before forwarding to your final endpoint
- Use the webhook secret to differentiate between multiple Polar organizations
How Hookdeck Can Help: Hookdeck's transformations allow you to add any custom headers to requests before forwarding them to your endpoint, eliminating the need for a custom proxy.
Redirects treated as delivery failures
The Problem: Polar does not follow HTTP redirects (301, 302, 307). Any redirect response is treated as a failed delivery, which counts toward the 10-failure auto-disable threshold.
Why It Happens: Following redirects on webhook deliveries introduces security risks (open redirect attacks) and adds unpredictable latency. Most webhook providers take this approach as a deliberate security measure.
Workarounds:
- Ensure your webhook URL points directly to the final destination with no redirects in the chain
- Check for
wwwvs. non-wwwredirects on your hosting provider (a common issue with Vercel and similar platforms) - Test your endpoint URL with
curl -vvv -X POST <url>to verify there are no redirects - If using a CDN or reverse proxy, configure it to handle the webhook path without redirects
How Hookdeck Can Help: Hookdeck provides stable, dedicated webhook URLs that never redirect. Your Polar configuration always points to Hookdeck, while Hookdeck reliably forwards to your actual endpoint regardless of any infrastructure changes on your side.
Base64 secret encoding gotcha
The Problem: The Standard Webhooks specification requires the webhook secret to be base64 encoded before generating the HMAC signature. Developers who implement custom verification logic (without using an official SDK) frequently encounter signature mismatches because they use the raw secret string.
Why It Happens: This is a requirement of the Standard Webhooks specification, not unique to Polar. However, it's a common stumbling block because it's easy to miss in the documentation, and most developers expect to use the secret as-is.
Workarounds:
- Use Polar's official SDKs (TypeScript, Python) which handle encoding automatically
- If implementing custom verification, base64 encode the secret before using it:
Buffer.from(secret, 'base64')in Node.js orbase64.b64decode(secret)in Python - Use the Standard Webhooks libraries available in many languages, which handle encoding correctly
How Hookdeck Can Help: Hookdeck supports Standard Webhooks signature verification natively. Configure your webhook secret in Hookdeck, and it handles signature validation before forwarding verified events to your endpoint.
No dedicated subscription renewal event
The Problem: There is no subscription.renewed event in Polar's webhook system. To detect subscription renewals, you must listen for order.created events and check the billing_reason field for the value subscription_cycle.
Why It Happens: Polar models renewals as new orders rather than subscription state changes, which aligns with their billing architecture but creates friction for developers who think in terms of subscription lifecycle events.
Workarounds:
- Listen for
order.createdand filter onbilling_reason === 'subscription_cycle' - Alternatively, use
subscription.updatedand comparecurrent_period_start/current_period_endchanges to detect when a new billing cycle has started - Combine both approaches for maximum reliability
How Hookdeck Can Help: Hookdeck's filter and transformation capabilities let you create a virtual "subscription renewed" event by filtering order.created events where billing_reason equals subscription_cycle and routing them to a dedicated handler, making your integration logic cleaner.
Cloudflare Bot Fight Mode blocks webhook deliveries
The Problem: If your endpoint sits behind Cloudflare with Bot Fight Mode enabled, Polar's webhook deliveries will be blocked with 403 errors. Standard mitigations like IP allowlisting and custom WAF rules do not override Bot Fight Mode.
Why It Happens: Cloudflare's Bot Fight Mode identifies and blocks automated HTTP requests, and Polar's webhook delivery infrastructure (correctly) appears as automated traffic.
Workarounds:
- Disable Bot Fight Mode entirely in your Cloudflare dashboard under Security > Bots
- Use a separate subdomain or path that bypasses Cloudflare for webhook endpoints
- Add Polar's production IPs to your firewall allowlist if you're not using Bot Fight Mode
How Hookdeck Can Help: Point your Polar webhooks at Hookdeck's infrastructure, which is purpose-built for receiving webhooks. Hookdeck then delivers to your Cloudflare-protected endpoint using its own delivery mechanism, avoiding Bot Fight Mode conflicts.
Testing Polar webhooks
Use the sandbox environment
Polar provides a full sandbox environment that mirrors production. Use it to test your webhook handlers with realistic data without processing real payments:
- Create test products and subscriptions
- Simulate purchases, cancellations, and refunds
- Verify your endpoint handles all event types correctly
- Test failure scenarios by temporarily returning error codes
Inspect payloads before building handlers
Before implementing your handler logic, use a request inspection tool like Hookdeck Console to capture and examine real Polar payloads:
- Create a temporary Hookdeck URL
- Configure it as your webhook endpoint in Polar's sandbox
- Trigger events by creating test purchases
- Inspect the exact payload structure and headers
Review delivery history
Use Polar's built-in delivery log to monitor webhook activity:
- View all past deliveries for each endpoint
- Inspect the actual payloads that were sent
- Manually re-trigger failed deliveries for debugging
- Verify that your endpoint is returning appropriate status codes
Test common failure scenarios
Validate that your integration handles edge cases:
- Duplicate deliveries (idempotency)
- Out-of-order events (e.g.,
order.paidarriving beforeorder.created) - Rapid successive events (e.g., create then immediate cancel)
- Payload signature verification failures
- Timeout behavior (ensure your handler responds within 2 seconds)
Conclusion
Polar webhooks provide a solid foundation for integrating billing events into your application. The Standard Webhooks specification ensures consistent signing and verification, the comprehensive event catalog covers the full subscription lifecycle, and official SDKs in multiple languages make implementation straightforward. The sandbox environment and delivery log give developers the tools they need to build and debug integrations with confidence.
However, the tight 10-second timeout, automatic endpoint disabling, lack of a dead letter queue, and absence of custom header support mean production deployments require careful architectural decisions. Processing webhooks asynchronously, implementing idempotency, and building reconciliation processes will address most common issues.
For straightforward integrations with reliable endpoints and moderate event volumes, Polar's built-in webhook system combined with proper error handling and the official SDKs works well. For mission-critical billing workflows where every event matters, high-volume scenarios, or complex multi-destination routing, webhook infrastructure like Hookdeck can address Polar's limitations — providing configurable timeouts, automatic queuing, dead letter preservation, payload transformation, and comprehensive delivery monitoring without modifying your Polar configuration.