← All guides
PayPalJuly 12, 20266 min read

Test PayPal Webhooks on Localhost Without Deploying

Receive, route, and transform PayPal webhooks on your machine. A paypal webhook localhost setup with signature headers preserved so verification works locally.

You wired up a PayPal integration, subscribed to PAYMENT.CAPTURE.COMPLETED, and now you need to confirm your handler actually fires when a payment lands. The problem: PayPal only speaks to public HTTPS URLs. Your handler lives on localhost:3000. So you deploy to staging, tail logs, tweak one line, redeploy, wait, repeat. Every iteration costs minutes you'll never get back.

Worse, PayPal signs each event with a set of Paypal-Transmission-* headers, and any tunneling hack that rewrites or drops those headers quietly breaks your signature verification — so you can't even test the part that matters most.

Relayers fixes both problems. It gives you a public ingest URL, forwards the original request body and headers untouched to your local machine, and lets you filter and reshape events before they hit your code.

What you'll build

PayPal never knows your code runs on a laptop. It just sees a stable public URL.

Prerequisites

1Create an endpoint in Relayers

Open the desktop app and go to Endpoints → Create. Give it a name like "PayPal Sandbox." Relayers generates a public ingest URL that looks like:

https://api.relayers.app/v1/webhooks/wep_8fk29dl3

Hit Copy to grab it. Before touching PayPal, click Send test to fire a sample payload at the endpoint and confirm it's live — you'll see it appear instantly in the Events view.

2Add the webhook in the PayPal Developer Dashboard

In the PayPal Developer Dashboard, go to My Apps & Credentials, open your app, and scroll to the Webhooks section. Click Add Webhook.

Save it. Every event PayPal sends arrives with its signature headers intact:

Paypal-Transmission-Id: ...
Paypal-Transmission-Sig: ...
Paypal-Transmission-Time: ...
Paypal-Cert-Url: ...
Paypal-Auth-Algo: ...

These are what the verify-webhook-signature API uses to prove the event came from PayPal. Relayers forwards them byte-for-byte, so verification behaves exactly as it will in production.

To generate traffic on demand, use the Webhooks Simulator in the dashboard. Pick an event type, point it at your endpoint, and send — no real transaction needed.

3Forward events to localhost

Authenticate the CLI once:

relayers login

Then open a tunnel that forwards incoming events to your local handler:

relayers listen --forward localhost:3000

Now trigger a simulated event (or make a real sandbox payment). It flows PayPal → Relayers → your machine in real time. Open the payload inspector in the desktop app to see the full request — body, headers, and status — for anything that comes through.

4Route and filter with JQ

You rarely want every event hitting your handler. In the Rules tab, add a JQ filter so only the events you care about get forwarded. To let through captured payments only:

{ "event_type": "PAYMENT.CAPTURE.COMPLETED" }

Each rule has a destination — a tunnel to localhost:PORT or a public URL — and rules match top to bottom, first match wins. Drag to reorder them. So you might send PAYMENT.CAPTURE.COMPLETED to localhost:3000, route BILLING.SUBSCRIPTION.* events to a different port, and drop everything else.

5Transform the payload

PayPal's payloads are deeply nested. If your local handler only needs a few fields, add a Transformation (jq, JSONata, JavaScript, or Go template) and shape the event before it's delivered. A jq transform to flatten a capture:

{
  event: .event_type,
  capture: .resource.id,
  amount: .resource.amount.value,
  currency: .resource.amount.currency_code
}

Use the built-in test playground to paste a real PayPal event and see the output before you save. Your handler now receives clean, predictable JSON instead of a nested tree.

A note on signature verification

This is the part most local-testing setups get wrong. PayPal's verify-webhook-signature API needs the exact Paypal-Transmission-Id, Paypal-Transmission-Sig, Paypal-Transmission-Time, Paypal-Cert-Url, and Paypal-Auth-Algo headers, plus the unmodified raw body and your webhook ID.

Because Relayers forwards the original body and headers untouched, you can run the full verification flow against localhost and get the same pass/fail result you'd get in production. If you use a filter rule without a transform, the body is delivered verbatim — ideal when your handler hashes the raw payload. (If you do transform the payload, verify the signature upstream in your rule logic, since verification depends on the original bytes.)

Going to production

When you're ready to ship, nothing about your integration changes on PayPal's side. Point a rule at your production URL instead of the tunnel, or keep the same endpoint and swap the destination. You can export your endpoint, rules, and transformations as a JSON bundle and import them into a production workspace, so the config you tested locally is the config that runs live. Switch your PayPal app from sandbox to live credentials and you're done.

Troubleshooting

Wrap-up

Testing PayPal webhooks on localhost doesn't require a deploy loop or a hack that breaks your signatures. Create an endpoint, register it in the PayPal dashboard, forward with the CLI, and filter and transform events to fit your handler — with the Paypal-Transmission-* headers preserved so verification works exactly as it will in production.

Ready to stop redeploying to test a webhook? Get started at relayers.app and download the app.