Provider guide

PayPal webhooks, kept properly.

PayPal signs its webhooks, but not in a way you can check with a shared secret and three lines of code. Verification means fetching a certificate and validating a chain, which is why so many integrations quietly skip it.

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.

01

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.

02

Register the capture URL

PayPal Developer Dashboard → your app → Webhooks → Add. Paste the capture URL and subscribe to the event types you need.

03

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 →

captured · PAYMENT.CAPTURE.COMPLETEDkept
# 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.

EventWhat it tells you
CHECKOUT.ORDER.APPROVEDThe buyer approved an order; it still has to be captured.
PAYMENT.CAPTURE.COMPLETEDFunds were captured.
PAYMENT.CAPTURE.DENIEDA capture was denied.
PAYMENT.CAPTURE.REFUNDEDA captured payment was refunded.
BILLING.SUBSCRIPTION.CANCELLEDA subscription was cancelled.
CUSTOMER.DISPUTE.CREATEDA 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.