Skip to main content

Guide

How to Test Paddle Webhooks Without Production

A practical guide to testing Paddle Billing webhook handlers — subscription lifecycle events, transactions, refunds — without merchant approval or shared test environments.

Apr 10, 2026FetchSandbox Engineering

Paddle webhook handlers are tricky for the same reason every webhook handler is tricky: the events arrive on a second timeline you do not control, they get retried, they sometimes arrive out of order, and the path you actually need to test is hard to trigger on demand.

This guide is the short version of how to test the Paddle webhook handlers that matter most, with code you can copy.

The Paddle webhook events you should care about

Paddle Billing fires a long list of events, but in practice the ones that break most integrations are:

  • subscription.activated — fires when a trial converts or a new subscription starts
  • subscription.paused — when a customer pauses billing
  • subscription.resumed — when a paused subscription resumes
  • subscription.canceled — at the end of the period
  • transaction.completed — successful charge
  • transaction.payment_failed — declined charge
  • adjustment.created — refund or partial refund

What usually breaks

  • signature verification running against the parsed body instead of the raw request body
  • retry behavior creating duplicate side effects (most common production bug)
  • handlers assuming events arrive in order — they do not
  • handlers assuming the API call result is canonical when the webhook is actually the source of truth
  • tests that only check the happy path and never replay an event twice

Three things your test setup needs

  1. Trigger events on demand. Paddle's official sandbox makes some events (especially failures) hard to fire reproducibly. You need a tool that lets you firesubscription.canceled when you want, not when the billing cycle decides.
  2. Replay events. Idempotency tests require sending the same event twice and confirming side effects only fire once.
  3. Per-test isolation. Your CI run cannot share state with your colleague's.

The minimum-viable Paddle webhook test

If you want a working Paddle sandbox right now:

# 1. Spin up a Paddle sandbox
curl -X POST https://fetchsandbox.com/api/sandboxes \
  -H "Content-Type: application/json" \
  -d '{"spec_id":"paddle"}'

# Returns: { id, base_url, credentials: [{ api_key }] }

Now point your Paddle client at the sandbox URL. POST a subscription. Watch your handler receive subscription.activated. Pause it, watch subscription.paused arrive. Cancel it, watch subscription.canceled arrive. Replay any of those events 10x and confirm your handler is idempotent.

The full lifecycle test as one integration test

The thing that broke for me on real Paddle's sandbox was running the entire subscription lifecycle as one repeatable test in CI. Create the customer. Create the product. Create the price. Create the subscription. Activate. Pause. Resume. Cancel. Verify the final state. Verify each webhook fired and was processed by your handler. Run it 100 times and confirm zero duplicates in the database.

That whole test runs in under 2 seconds against a stateful sandbox. The same test against the official Paddle sandbox is closer to 5 minutes per run, has shared state across your team, and depends on Paddle's server uptime.

We built a small showcase that does exactly this — see the Paddle billing demo on GitHub.

Where to go from here