All articles
A sequence of simulated billing lifecycle events moving through a secure tunnel to a local Paddle webhook handler.
Paddlewebhook simulatorbillinglocal testing

Use the Paddle Webhook Simulator on Localhost

To use the Paddle webhook simulator with localhost, expose your local handler through an HTTPS tunnel, create a sandbox notification destination that points to the public URL, then run a single-event or lifecycle simulation against that destination. Paddle sends a realistic request with a Paddle-Signature header, so the same raw-body verification path can run locally and in production.

Why Paddle's simulator is more than a sample payload

A copied JSON fixture tests your switch statement. Paddle's simulator tests the network destination, provider headers, signing secret, response status, and event schema together. The official webhook simulator overview supports reusable single events and scenarios. Single events target one event type and can be customized after a run. Scenarios send a predefined sequence for a lifecycle such as subscription creation or renewal.

Scenario configuration can populate payloads with existing Paddle entities and vary the flow. Each run produces simulation-run event records where you can inspect the payload, outbound request, and endpoint response. That makes the simulator useful for debugging state transitions, not merely proving that a route is online.

Connect a local endpoint to Paddle sandbox

  1. Start your application, for example on http://localhost:3000.
  2. Add a POST route such as /api/webhooks/paddle.
  3. Run npx portpreview 3000 and copy the HTTPS address.
  4. In Paddle sandbox, create a notification destination using https://your-subdomain.portpreview.dev/api/webhooks/paddle.
  5. Reveal and copy that destination's endpoint secret into PADDLE_WEBHOOK_SECRET. It is not your API key.
  6. Open Developer tools → Simulations, choose a single event or scenario, select the destination, configure it, and run it.
  7. Compare Paddle's simulation-run response with your local logs and persisted webhook receipt.

Use sandbox resources and credentials throughout. Mixing a live destination secret with a sandbox simulator request guarantees a signature failure even though both strings look plausible.

Verify the Paddle-Signature header

Paddle signs each webhook with the secret belonging to the notification destination. The header contains components including a Unix timestamp identified by ts and one or more signatures identified by h1. Paddle may add signature versions, so parse the header rather than assuming it contains one bare hash.

The signed payload is the timestamp, a colon, and the untouched request body. Paddle applies HMAC-SHA256 with the endpoint secret. Its signature verification documentation recommends an official SDK where available and documents the manual algorithm. Always enforce the SDK's timestamp tolerance or an explicit tolerance in your verifier to limit replay attacks.

Prefer the official SDK in Node

import express from 'express';
import { Environment, Paddle } from '@paddle/paddle-node-sdk';

const app = express();
const paddle = new Paddle(process.env.PADDLE_API_KEY, {
  environment: Environment.sandbox,
});

app.post(
  '/api/webhooks/paddle',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.header('paddle-signature') ?? '';
    const rawBody = req.body.toString('utf8');

    try {
      const event = await paddle.webhooks.unmarshal(
        rawBody,
        process.env.PADDLE_WEBHOOK_SECRET,
        signature,
      );

      await acceptOnce(event.eventId, event);
      return res.status(200).send('accepted');
    } catch (error) {
      return res.status(400).send('invalid webhook');
    }
  },
);

app.listen(3000);

Mount express.raw() before any global express.json() middleware for this route. In a Fetch-style framework such as Next.js App Router, use await request.text() once and pass that exact string to the SDK. The official webhook quickstart demonstrates this raw body requirement.

What manual verification must do

  1. Read the raw body without JSON normalization.
  2. Parse every semicolon-separated header component and collect supported h1 values.
  3. Reject a missing, malformed, or unreasonably old ts.
  4. Compute HMAC_SHA256(secret, ts + ':' + rawBody).
  5. Compare the expected digest with candidate signatures using a timing-safe comparison.
  6. Only after a match, parse JSON and dispatch on event_type.

Accepting any valid supported signature matters during secret rotation, when more than one signature can be present. Do not split the header and blindly take the second item. Do not log the endpoint secret or complete customer payload while diagnosing a mismatch.

Design simulations around billing invariants

Subscription creation

Run a subscription-creation scenario and verify that your database can receive related transaction and subscription events without assuming one network arrival order. Store Paddle entity IDs and update records idempotently. The application should grant exactly one entitlement even if a request is resent.

Renewal and payment failure

Exercise successful renewal, past-due, and recovery paths available in your scenario configuration. Separate billing status from product access policy: your grace period may intentionally keep access active while Paddle reports a collection problem.

Cancellation and scheduled changes

Distinguish a subscription scheduled to cancel from one that has reached its effective cancellation. Preserve next_billed_at, scheduled-change data, and provider status rather than collapsing the lifecycle into is_active.

Entity updates

Simulate product, price, customer, and subscription updates that your cache consumes. A handler should ignore unknown event types safely and acknowledge them if the signature is valid; future Paddle additions should not turn into an endless failure loop.

Idempotency and ordering on every run

Use the webhook event's stable ID as a unique key in a receipt table. In one transaction, insert the receipt, apply the state change, and enqueue side effects. If insertion conflicts, return success without repeating work. Do not deduplicate only by entity ID because many legitimate updates can target one subscription.

Events can arrive out of order. Compare the event's occurrence time or retrieve the latest Paddle entity before applying a destructive transition. Store the provider's state and timestamp, and reject an older update that would overwrite a newer one. Simulation scenarios are ideal for testing these assumptions because they produce connected sequences rather than isolated fixtures.

Troubleshoot simulator-to-localhost failures

Destination returns 404 or 405

Check that the public URL includes the full route and that the route exports POST. A browser GET is not a valid test of a POST-only webhook. Use curl -X POST against the public URL to distinguish tunnel routing from Paddle configuration.

Destination returns 400

Inspect whether Paddle-Signature arrived and confirm the endpoint secret belongs to the selected notification destination. Ensure no body parser, request logger, or middleware consumed or reformatted the body. The simulator sends verifiable signatures, as documented in Paddle's simulation run guide.

Destination returns 500 or times out

Persist quickly and move email, provisioning, and outbound API calls to a queue. Return 2xx after durable acceptance. Throwing on an unfamiliar event type is a common cause of unnecessary failures; use a default branch that records and acknowledges it.

The scenario uses unexpected data

Review whether the simulation uses automatically generated values or is populated with real sandbox entities. Inspect the configured scenario options and the run event payload rather than assuming it mirrors a previous run.

Security checklist before production

  • Keep API keys and notification destination secrets separate and server-only.
  • Verify the raw body, header signature, and timestamp before processing.
  • Use a timing-safe comparison or official SDK verifier.
  • Deduplicate event IDs and handle out-of-order updates.
  • Apply body limits, POST-only routing, redacted logging, and rate controls.
  • Use distinct sandbox and live destinations, then repeat simulations and real sandbox flows before switching the production URL.

The simulator proves your integration under controlled lifecycle sequences; a real sandbox checkout additionally proves checkout-to-entity relationships. Use both, then review the general webhook signature guide and idempotency guide before launch.

Frequently asked questions

Can Paddle's webhook simulator send events to localhost?
Yes, through a public HTTPS tunnel. Configure a sandbox notification destination with the tunnel URL and route, then select that destination when running the simulation.
Are Paddle simulator webhook signatures real?
Yes. Paddle says the simulator sends a replica webhook request including Paddle-Signature. Verify it with the secret for the selected notification destination, just like a normal webhook.
What is the difference between a Paddle single-event simulation and a scenario?
A single-event simulation sends one customizable event. A scenario sends a predefined sequence representing a lifecycle such as subscription creation or renewal and can use existing sandbox entities.
Why does Paddle signature verification fail locally?
The usual causes are using the wrong destination secret, parsing the body before verification, omitting Paddle-Signature, mixing sandbox and live configuration, or passing a stale timestamp to a verifier with replay tolerance.