Field notes
How PayPal behaves, and where it bites
A signature scheme that is genuinely harder than everyone else's, and an event type buried in the body.
How PayPal sends
PayPal POSTs JSON with the event in event_type (PAYMENT.CAPTURE.COMPLETED, BILLING.SUBSCRIPTION.CANCELLED) alongside a set of PAYPAL-TRANSMISSION-* headers carrying the signature, the certificate URL and the algorithm.
Where it bites
- Verification is a chain, not a secret. It needs the certificate fetched and validated, or a call back to PayPal to verify the transmission. Neither is a one-liner.
- Sandbox and live have different webhook ids. Configuration that works in sandbox fails in live in a way that looks like the webhook is broken.
- The event type is in the body. Header-only logging tells you nothing about what happened.
The vault under it
Point a WebhookVault endpoint at PayPal and every delivery is stored in full: headers verbatim, bodies byte-for-byte, labelled with its event type and searchable. Forward it onward to your own handler if you want the relay pattern, replay any delivery once the bug is fixed, and let a Watchdog notice the day it stops arriving.
Create an endpoint
Name it paypal-payments. Turn on forwarding to your own handler if you want the vault in front of it rather than beside it.
Register the capture URL
PayPal Developer Dashboard → your app → Webhooks → Add. Paste the capture URL and subscribe to the event types you need.
Keep everything
Every delivery from here on is on the shelf: searchable, replayable, and watched for the day it goes quiet.
Kept whole, verdict or not
PayPal's certificate-chain scheme is one WebhookVault does not rule on yet, so its captures carry an unverifiable verdict that names the scheme rather than a false pass. That is deliberate: a verdict you cannot trust is worse than an honest blank.
Every header is still stored verbatim, so the proof sits with the payload whether or not the vault can rule on it. How verification works →
# every header kept, verdict withheld "PAYPAL-TRANSMISSION-SIG": "lmI95J…" "PAYPAL-CERT-URL": "https://api.paypal.com/…" "PAYPAL-AUTH-ALGO": "SHA256withRSA" { "id": "WH-58D329510W468432D…", "event_type": "PAYMENT.CAPTURE.COMPLETED", "resource": { "status": "COMPLETED", "amount": { "currency_code": "USD", "value": "49.00" } } }
Event catalogue
PayPal webhook events you'll see
A few of the events PayPal sends, not the full list: PayPal's own documentation has every one. Each arrives in the vault labelled with its event type.
| Event | What it tells you |
|---|---|
| CHECKOUT.ORDER.APPROVED | The buyer approved an order; it still has to be captured. |
| PAYMENT.CAPTURE.COMPLETED | Funds were captured. |
| PAYMENT.CAPTURE.DENIED | A capture was denied. |
| PAYMENT.CAPTURE.REFUNDED | A captured payment was refunded. |
| BILLING.SUBSCRIPTION.CANCELLED | A subscription was cancelled. |
| CUSTOMER.DISPUTE.CREATED | A buyer opened a dispute. |
Questions
PayPal webhook questions, answered
The three that come up most when PayPal is wired to a new endpoint.
How does PayPal retry webhooks?
If your endpoint does not answer with a 2xx, PayPal resends the event, up to 25 times over three days. A WebhookVault capture URL answers as soon as the event is stored, so the delivery is kept even while your own handler is failing.
How do I verify a PayPal webhook?
Either send the PAYPAL-TRANSMISSION-* headers, your webhook id and the event to PayPal's verify-webhook-signature API, or check it yourself: fetch the certificate from PAYPAL-CERT-URL, validate its chain, and verify the signature over the transmission id, time, webhook id and a CRC32 of the body. The vault keeps every one of those headers but does not rule on this scheme, so PayPal rows carry an unverifiable verdict rather than a false pass.
Why does my webhook work in sandbox but not in live?
Sandbox and live are separate apps with separate webhook ids and credentials. A webhook id copied from sandbox fails verification in live, and the live app has to subscribe to its events again. Point a capture URL at each environment and compare what arrives.
Keep the transmission headers, whatever you do with them.
Free tier, no card. Every header stored verbatim, for as long as your plan's retention window lasts.
Sign up with GitHub, Google or email. Two-factor authentication is required on every account, Free included, for account security.