PayPal webhook not working?

PayPal webhooks fail in ways that produce no error anywhere you'd think to look: the delivery silently never happens because your URL isn't on port 443, or it happens and your signature check fails because you verified against re-serialized JSON, or you tested with the simulator and verification can't pass the way you wrote it. Here's each failure mode, fastest check first.

1. First, separate "PayPal isn't sending" from "my endpoint is broken"

Point the webhook at a neutral capture URL so your code is out of the loop. Create a bin (button below, or curl https://hookden.pages.dev/new), subscribe it as a listener URL in Application management under your sandbox app, and fire a test event (or use the simulator — but read step 4 first).

You'll see the full delivery: paypal-transmission-id, paypal-transmission-time, paypal-transmission-sig, paypal-cert-url, paypal-auth-algo headers plus the exact raw JSON body — which matters more than usual with PayPal, because verification hashes the raw bytes (step 5).

2. Nothing arrives: the HTTPS-port-443 rule

PayPal delivers webhooks to HTTPS on port 443 only. A listener on https://example.com:8443/hook or plain HTTP will simply never receive anything — there's no error event for "we didn't try."

3. Deliveries marked Failed: the 2xx rule and the 3-day window

Your listener must answer every delivery with an HTTP 2xx. Any other outcome — no response, connection refused, 404, 500 — makes PayPal retry that delivery up to 25 times over 3 days. After 3 days it's marked Failed, but you can resend it manually from the Webhook Events view in the dashboard.

4. Simulator events fail verification — by design

The Webhooks simulator generates mock events that don't belong to any app, and two things about them break naive verification:

So: a verification failure on simulator traffic is not evidence your code is wrong, and a pass on simulator traffic with WEBHOOK_ID hardcoded will fail in production. Test the crypto with real sandbox events (subscribe the listener to a sandbox app and trigger a real payment event).

5. Signature verification: what's actually signed

PayPal doesn't use a shared-secret HMAC like Stripe or Shopify. The signed message is a pipe-joined string:

transmissionId | timeStamp | webhookId | crc32

Then verify paypal-transmission-sig (base64) against that string using the public key from the cert at paypal-cert-url (algorithm per paypal-auth-algo, typically SHA256withRSA). Alternatively, post the whole thing back to /v1/notifications/verify-webhook-signature — but the webhook_event you send must be byte-for-byte what you received; re-serialized JSON can fail there too.

A captured delivery in a bin preserves the raw bytes exactly, so you can compute the CRC32 against reality, copy the request as curl, and replay it at your real handler while you debug. (PayPal's scheme is RSA-cert-based, so the bin's shared-secret HMAC badge doesn't apply here — use the raw body + headers it preserves.)

6. Developing locally? PayPal can't reach localhost

A public HTTPS URL on port 443 means localhost:3000 is out. Instead of running a tunnel, give PayPal a stable capture URL and relay deliveries to your machine:

$ curl -s https://hookden.pages.dev/cli -o hookden && chmod +x hookden
$ curl https://hookden.pages.dev/new
$ ./hookden relay YOUR_BIN http://localhost:3000

Bodies are re-delivered byte-identical, so the CRC32 in your verification code still matches what PayPal signed. Details: webhooks to localhost without a tunnel.

No signup needed. Or from your terminal: curl https://hookden.pages.dev/new

← All guides · Docs · Hookden vs webhook.site