docs
IntegrateWebhooks

Local development

Tunnel webhook deliveries to your laptop while you build. Build, fix, replay, repeat.

XPay can't deliver webhooks to localhost. To exercise your handler against real test-mode events while you build, expose your dev server through a tunnel that gives you a public HTTPS URL, point a test-mode webhook endpoint at that URL, and iterate.

The flow looks like this:

  1. Run your handler locally on a port (say 3000).
  2. Run a tunnel that forwards a public HTTPS URL to that port.
  3. In the dashboard, in test mode, create a webhook endpoint pointing at the tunnel URL.
  4. Trigger events (test payment, refund, etc.) and watch them land on your laptop.
  5. When something's broken, fix the handler and Resend the failed event from the Workbench. No need to re-trigger the original action.

Set up a tunnel

Any tool that forwards a public HTTPS URL to a local port works. Two common picks:

ToolStable URLNotes
Cloudflare TunnelYes, when you create a named tunnelFree. A named tunnel keeps the same hostname across restarts. Best for long-running dev. CLI is cloudflared.
ngrokRandom per restart on the free planFast to start. The free plan rotates the hostname every restart, so you re-edit the endpoint URL each time.

Pick whichever you already have. The webhook flow doesn't care which one signs your TLS cert.

Run your handler

Start your dev server on whatever port your handler listens on. The examples below use 3000.

Start a quick tunnel

The simplest path: a one-shot tunnel that gives you a random *.trycloudflare.com URL.

cloudflared tunnel --url http://localhost:3000

The command prints a public URL in the form https://random-words-1234.trycloudflare.com. Copy it.

For a stable hostname across restarts, set up a named tunnel and config file (Cloudflare's docs). Once configured, your tunnel keeps the same URL forever.

Add the URL as a webhook endpoint

In the dashboard, switch to Test mode, open Developers → Webhooks, and click Add endpoint. Use:

  • Endpoint URL: https://random-words-1234.trycloudflare.com/webhooks/xpay (your tunnel URL plus your handler's path).
  • Events: pick the ones your handler cares about. checkout.session.completed is the usual starting point.

Click Add endpoint, copy the whsec_* from the dialog, and paste it into your dev env (.env.local, direnv, or whatever you use):

XPAY_WEBHOOK_SECRET=whsec_...

Restart your dev server so the env var loads.

Run your handler

# In the terminal where your dev server is
pnpm dev # or whatever you use

Open a tunnel

ngrok http 3000

The terminal shows a Forwarding line with a https://*.ngrok-free.app URL. Copy it.

Add the URL as a webhook endpoint

In the dashboard's Test mode, open Developers → Webhooks → Add endpoint, paste the tunnel URL, pick events, copy the whsec_* into your env, restart your handler.

Update the URL each time you restart ngrok

The free plan rotates the public hostname every time ngrok http restarts. When that happens:

  • Open the endpoint in Developers → Webhooks, click the menu, pick Edit webhook, and replace the URL.
  • The whsec_* does not change on edit, so your handler's env stays the same.

Trigger events to test

Anything that fires a webhook in production fires the same event in test mode. The fastest paths to drive traffic at your handler:

  • Take a test payment. Create a Payment Link in the dashboard, open it, pay with the success card 5123 4500 0000 0008 and expiry 01/39. See Test mode and test cards for the full matrix and the failure expiries.
  • Issue a test refund. From the test-mode dashboard, open a successful transaction and click Refund. The handler receives refund.created and charge.refunded.
  • Replay an event. The Workbench's Events tab keeps every test-mode event for 90 days. Click Resend on any delivery to feed your handler that exact payload again. See Replaying & retries.

A typical loop while building: take one test payment, watch it hit your handler, fix what's broken, click Resend, repeat. You don't need to take a fresh payment for every iteration.

Sign-off checklist before flipping to live

Before you stop testing locally and move to live mode, verify each of these against your tunnel-delivered events:

  • Signatures are checked. Your verifier returns 400 on a tampered body. Test it manually by mutating one byte of a captured request.
  • Replays are idempotent. Click Resend on the same event five times. Your fulfillment runs once.
  • Bad signatures don't crash the handler. Returning 400 is fine; throwing 500 because of a header parse error is a security smell.
  • The handler acks within 30 seconds. If your work takes longer, push it to a queue and return 200 immediately. XPay considers anything slower a failure and retries.
  • The signing secret is not committed. It lives in a local env file or a secret manager, not in your repo.

When all five pass against test events, you're ready to add a separate live-mode endpoint with its own URL and its own whsec_*. Keep both endpoints subscribed to the same events; develop and deploy independently.

Where to next

On this page