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

# Handle & monitor webhook deliveries

> Receive, verify and react to Macropay webhooks — with delivery monitoring, retries and troubleshooting built in

<img className="block dark:hidden" src="https://mintcdn.com/macrodeepinc/Uu6hOe0dMZnKqgkh/assets/integrate/webhooks/delivery.light.png?fit=max&auto=format&n=Uu6hOe0dMZnKqgkh&q=85&s=f75f558a32783a4510be6808390bb174" width="2740" height="1522" data-path="assets/integrate/webhooks/delivery.light.png" />

<img className="hidden dark:block" src="https://mintcdn.com/macrodeepinc/Uu6hOe0dMZnKqgkh/assets/integrate/webhooks/delivery.dark.png?fit=max&auto=format&n=Uu6hOe0dMZnKqgkh&q=85&s=3f51594c3ce01ffbd7928115e038fe09" width="2672" height="1526" data-path="assets/integrate/webhooks/delivery.dark.png" />

Webhooks are how your app learns what happened. As your Merchant of Record,
Macropay settles the payment, remits the sales tax or VAT, and absorbs the PCI
and dispute handling — then fires an event so your code can do its part: grant a
license, unlock a download, provision a seat, or top up prepaid credits. The
same applies to usage- and agent-driven billing: when an order or subscription
moves, a webhook is your signal to react.

This page covers the full lifecycle: building a receiver that verifies and
parses events, the network and reliability guarantees you can rely on, and how
to monitor and debug deliveries from the dashboard.

## Monitor deliveries in the dashboard

Every registered endpoint gets a delivery overview. From there you can:

* Browse the full history of delivery attempts
* Inspect the exact payload that was sent
* Replay any delivery — useful after you ship a fix for a failing handler

Keep this open while you wire up your route below; it's the fastest way to
confirm events are leaving Macropay and to see the status code your endpoint
returned.

## Build your receiver

Register a route at the URL you configured on Macropay, then verify the
signature, parse the payload, and branch on the event type before doing any
work.

### With the SDKs

The TypeScript and Python SDKs ship a single helper that verifies the signature
and returns a typed event in one step.

<CodeGroup>
  ```typescript icon="square-js" JS (Express) theme={null}
  import express, { Request, Response } from 'express'
  import { validateEvent, WebhookVerificationError } from '@macropayments/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['MACROPAY_WEBHOOK_SECRET'] ?? '',
        )

        // Branch on the event and hand off to a background worker
        switch (event.type) {
          case 'order.paid':
            // e.g. queue entitlement grant for event.data.customer_id
            break
          case 'subscription.canceled':
            // e.g. queue access revocation
            break
        }

        // Acknowledge fast — return 2xx before doing heavy work
        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 macropay_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('MACROPAY_WEBHOOK_SECRET', ''),
          )

          # Branch on the event and hand off to a background worker
          if event.type == 'order.paid':
              ...  # e.g. enqueue entitlement grant for event.data.customer_id
          elif event.type == 'subscription.canceled':
              ...  # e.g. enqueue access revocation

          # Acknowledge fast — return 2xx before doing heavy work
          return "", 202
      except WebhookVerificationError:
          return "", 403
  ```
</CodeGroup>

<Tip>
  Both samples read the signing secret from an environment variable named
  `MACROPAY_WEBHOOK_SECRET`. Set it to the secret shown when you created the
  endpoint.
</Tip>

### Rolling your own verification

Macropay signs every event using the
[Standard Webhooks](https://www.standardwebhooks.com/) specification, so you can
drop in any of the
[community libraries](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries)
across languages — or follow the
[spec](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md)
directly if you'd rather implement it yourself.

<Info>
  **The secret must be base64-encoded**

  The most common mistake when verifying by hand is forgetting that the signing
  secret is base64-encoded before the signature is computed. The SDK handles this
  for you; if you roll your own, decode the secret first.
</Info>

## Network allowlist

If a firewall or reverse proxy sits in front of your endpoint, allow the
following ranges so deliveries can reach you.

<Danger>
  **New IP ranges**

  Starting **October 27th, 2025**, these ranges are added to the list:

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

## Reliability guarantees

Macropay does the heavy lifting to get each event delivered. Here's what to
expect, and what's expected of your handler.

| Behavior                      | Detail                                                                                  |
| ----------------------------- | --------------------------------------------------------------------------------------- |
| **Retries**                   | Up to **10 attempts** with exponential backoff on any non-2xx response or network error |
| **Timeout**                   | Requests are cut off after **10 seconds**, which triggers a retry                       |
| **Recommended response time** | Acknowledge within **2 seconds**                                                        |
| **Auto-disable**              | An endpoint is disabled after **10 consecutive failed deliveries**                      |

<Warning>
  **Respond fast, work later**

  The timeout may tighten over time. Treat the webhook handler as a fast
  acknowledgement layer: verify, enqueue a background task, and return a 2xx
  immediately. Doing real work inline risks timeouts and unnecessary retries.
</Warning>

### When an endpoint is disabled

After 10 consecutive non-2xx responses, the endpoint stops receiving new events
and your organization's admins get an email notification. To recover:

1. Confirm the endpoint is reachable and returning 2xx (use redelivery from the
   dashboard to test).
2. Re-enable it under your organization's webhook settings.

Re-enabling before the underlying issue is fixed will simply trip the
auto-disable again.

## Troubleshooting

Seeing deliveries leave Macropay but nothing arriving on your end? Start with
the basics, then match the reported status code below.

**First checks**

* **Tunnel still live?** When testing locally, confirm your tunnel (ngrok,
  Cloudflare Tunnel, etc.) is up and its public URL still matches the endpoint
  configured on Macropay.
* **Log generously.** Emit markers like `webhook.handler_called`,
  `webhook.signature_validated`, and `webhook.handled` so you can see whether
  the handler ran and how far it got.

**By status code**

| Status | Likely cause & fix                                                                                                                                                                                                                             |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404`  | Route not found. Run `curl -vvv -X POST <endpoint-url>` to confirm it exists; many frameworks resolve `/foo` to `/foo/`, so try adding a trailing slash on Macropay.                                                                           |
| `3xx`  | Redirects (301, 302, 307…) count as failures — Macropay does not follow them. Point the webhook at the final URL. A frequent culprit is host-level `www` ↔ non-`www` redirects (e.g. on Vercel); match your configured URL to the real domain. |
| `403`  | Auth middleware or a bot filter is blocking the request. See below.                                                                                                                                                                            |

**Resolving `403`**

* If you protect routes with authorization middleware, exclude the webhook path
  — it must be publicly reachable.
* On Cloudflare:
  * Check the firewall logs to see whether requests are blocked, and add a WAF
    rule to accept traffic from Macropay's IP ranges.
  * **Bot Fight Mode** is a common cause: it blocks legitimate webhook traffic,
    and neither IP allowlisting nor custom WAF rules override it. Disable it
    under **Security → Bots**, then re-test.

### Invalid signature errors

Verifying signatures yourself? Base64-decode the secret you configured on
Macropay before computing the signature — a raw secret will never match.
