# 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:

1. `subscription.updated`
2. `subscription.canceled`

The subscription retains `active` status with `cancel_at_period_end` set to `true`. When the billing period ends:

1. `subscription.updated`
2. `subscription.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:

1. `subscription.updated`
2. `subscription.canceled`
3. `subscription.revoked`

### Renewal sequence

When a subscription renews:

1. `subscription.updated` — reflects the new `current_period_start` and `current_period_end`
2. `order.created` — the invoice for the new cycle (status: `pending`)
3. `order.updated` — once payment processes (status: `paid`)
4. `order.paid` — confirmation of successful payment

## Setting up Polar webhooks

### Via the Polar dashboard

1. Navigate to your organization's Settings page
2. Click the Add Endpoint button
3. Enter the URL where webhook events should be sent
4. 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.
5. Set your secret — either provide your own or let Polar generate a random one. This is used to cryptographically sign every delivery.
6. 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 delivery
* `webhook-timestamp` — Unix timestamp of when the webhook was sent
* `webhook-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:

```json
{
  "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):

```javascript
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:

```python
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:

```javascript
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:

```javascript
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 `202` response 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 `2xx` status, 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](/webhooks/guides/dead-letter-queues-webhook-reliability) 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 `www` vs. non-`www` redirects 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 or `base64.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.created` and filter on `billing_reason === 'subscription_cycle'`
* Alternatively, use `subscription.updated` and compare `current_period_start` / `current_period_end` changes 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](https://console.hookdeck.com) to capture and examine real Polar payloads:

1. Create a temporary Hookdeck URL
2. Configure it as your webhook endpoint in Polar's sandbox
3. Trigger events by creating test purchases
4. 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.paid` arriving before `order.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](https://hookdeck.com) can address Polar's limitations — providing configurable timeouts, automatic queuing, dead letter preservation, payload transformation, and comprehensive delivery monitoring without modifying your Polar configuration.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Polar webhooks reliably in minutes.