# Source: https://polar.sh/docs/integrate/webhooks/delivery.md

> ## Documentation Index
> Fetch the complete documentation index at: https://polar.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle & monitor webhook deliveries

> How to parse, validate and handle webhooks and monitor their deliveries on Polar

<img className="block dark:hidden" src="https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=1c5f560bcca11607c67cc7f8b467dca4" data-og-width="2740" width="2740" data-og-height="1522" height="1522" data-path="assets/integrate/webhooks/delivery.light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?w=280&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=61c5f66e8cb7acab698915171b5bde4e 280w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?w=560&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=f452e9e0edebfa7b912f85238d9d7f3c 560w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?w=840&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=49d406e3c294cb33ac178f7c61cd81aa 840w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?w=1100&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=f2a6770fba4cc61303cc7d84de7db7bb 1100w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?w=1650&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=609d960dff8b2cf958e52118b7747365 1650w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.light.png?w=2500&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=683818fb66539342505f27574af611a5 2500w" />

<img className="hidden dark:block" src="https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=4ce4ac52095eb88c9073c4a18099a0bf" data-og-width="2672" width="2672" data-og-height="1526" height="1526" data-path="assets/integrate/webhooks/delivery.dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?w=280&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=623e1747b6245fd84986462620718bcc 280w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?w=560&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=da4785f9e803826575e0a148286e3c43 560w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?w=840&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=0d3e453abcd767cddea396717bc774ec 840w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?w=1100&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=314fcf0248f280018d16f61ff50cb86c 1100w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?w=1650&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=aa74acbb1c73448868054df87bd7e0ad 1650w, https://mintcdn.com/polar/0Af3hN6-oIM4IHT3/assets/integrate/webhooks/delivery.dark.png?w=2500&fit=max&auto=format&n=0Af3hN6-oIM4IHT3&q=85&s=9117a6ffafa146e98eed1f1f1582a776 2500w" />

Once a webhook endpoint is setup you will have access to the delivery overview
page. Here you can:

* See historic deliveries
* Review payload sent
* Trigger redelivery in case of failure

Now, let's integrate our endpoint route to validate, parse & handle incoming webhooks.

## Validate & parse webhooks

You now need to setup a route handler for the endpoint registered on Polar to
receive, validate and parse webhooks before handling them according to your
needs.

### Using our SDKs

Our TypeScript & Python SDKs come with a built-in helper function to easily
validate and parse the webhook event - see full examples below.

<CodeGroup>
  ```typescript icon="square-js" JS (Express) theme={null}
  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

        res.status(202).send('')
      } catch (error) {
        if (error instanceof WebhookVerificationError) {
          res.status(403).send('')
        }
        throw error
      }
    },
  )
  ```

  ```python Python (Flask) theme={null}
  import os
  from flask import Flask, request
  from polar_sdk.webhooks import validate_event, WebhookVerificationError

  app = Flask(__name__)

  @app.route('/webhook', methods=['POST'])
  def webhook():
      try:
          event = validate_event(
              body=request.data,
              headers=request.headers,
              secret=os.getenv('POLAR_WEBHOOK_SECRET', ''),
          )

          # Process the event

          return "", 202
      except WebhookVerificationError as e:
          return "", 403
  ```
</CodeGroup>

Both examples above expect an environment variable named `POLAR_WEBHOOK_SECRET`
to be set to the secret you configured during the endpoint setup.

### Custom validation

We follow the [Standard Webhooks](https://www.standardwebhooks.com/)
standard which offers [many libraries across languages](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) to
easily validate signatures. Or you can follow their
[specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md)
in case you want to roll your own.

<Info>
  **Note: Secret needs to be base64 encoded**

  One common gotcha with the specification is that the webhook secret is expected to be
  base64 encoded. You don't have to do this with our SDK as it takes care of the
  implementation details with better developer ergonomics.
</Info>

## IP Allowlist

If you are using a firewall or a reverse proxy that requires IP allowlisting, here are the IPs range you need to allow:

<Danger>
  **New IP ranges**

  Starting **October 27th, 2025**, new IP ranges will be added:

  ```
  74.220.50.0/24
  74.220.58.0/24
  ```
</Danger>

<CodeGroup>
  ```txt Production theme={null}
  3.134.238.10
  3.129.111.220
  52.15.118.168

  74.220.50.0/24
  74.220.58.0/24
  ```

  ```txt Sandbox theme={null}
  3.134.238.10
  3.129.111.220
  52.15.118.168

  74.220.50.0/24
  74.220.58.0/24
  ```
</CodeGroup>

## Failure Handling

### Delivery Retries

If we hit an error while trying to reach your endpoint, whether it is a temporary network error or a bug, we'll retry to send the event up to **10 times** with an exponential backoff.

### Delivery Timeouts

We currently timeout our requests to your endpoint after **10 seconds**, triggering a
retry attempt after a delay as explained above. However, we strongly recommend you optimize your endpoint route to respond within **2 seconds** to ensure reliable delivery. We may lower the timeout threshold in the future, so we advise implementing your webhook handler to queue a background worker task to handle the
payload asynchronously.

### Endpoint Disabling

Webhook endpoints are automatically disabled after **10 consecutive failed deliveries** (non-2xx responses). When this happens:

* The endpoint is marked as disabled and will no longer receive new events.
* Admin of the organization will receive an email notification.

To re-enable a disabled endpoint, go to your organization's webhook settings in the dashboard and manually enable it. Before re-enabling, ensure your endpoint is properly configured and reachable to avoid repeated disabling.

## Troubleshooting

### Not receiving webhooks

Seeing deliveries on Polar, but not receiving them on your end? Below are some
common techniques to resolve the issue depending on the reported error status.

**General**

*Start ngrok or similar*

Make sure you have started `ngrok` or whatever tunneling service you're using
during local development.

*Add excessive logging*

E.g
`console.log('webhook.handler_called')`,
`console.log('webhook.validate_signature')`,
`console.log('webhook.signature_validated')` etc.

So you can easily confirm if the handler is called and how far it gets before
any issues arise.

`HTTP 404`

* Try `curl -vvv -X POST <copy-paste-endpoint-url>` in your terminal to confirm the
  route exists and see any issues along the way
* Try adding trailing `/` to the URL on Polar. Often `/foo` is resolved to
  `/foo/` by frameworks.

`HTTP 3xx`

Redirect responses (301, 302, 307, etc.) are treated as failures. Polar does not follow redirects for webhook deliveries. Update your webhook URL to the final destination URL to avoid redirects.

A common cause is hosting providers like Vercel that redirect between `www` and non-`www` domains. Make sure your configured URL matches your actual domain.

`HTTP 403`

* Using middleware for authorization? Make sure to exclude the webhook route
  from it since it needs to be publicly accessible
* Using Cloudflare?
  * Check the firewall logs to verify if they are blocking our requests and setup a custom WAF rule to accept incoming requests from Polar.
  * Webhook delivery failures with 403 errors can occur when Cloudflare's Bot Fight Mode is enabled. Bot Fight Mode automatically blocks requests it identifies as bots, including legitimate webhook requests from Polar. Adding Polar's IP addresses to your IP Allow List or creating custom WAF rules will not resolve this issue. To fix webhook delivery problems, disable Bot Fight Mode in your Cloudflare dashboard under Security > Bots. Alternatively, you can check your Cloudflare firewall logs to confirm if requests are being blocked and create appropriate firewall rules if needed.

### Invalid signature exceptions

Rolling your own webhook validation logic? Make sure to base64 encode the secret
you configured on Polar in your code before generating the signature to validate
against.