How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a PayPal webhook?
A PayPal webhook is an HTTP POST that PayPal sends to a listener URL registered on a REST app, announcing that something happened to a payment: a capture completed, a refund went through, a dispute was opened, a subscription was cancelled. Every notification shares one envelope — id, event_type, create_time, resource_type, summary — with the affected object in resource.
These are REST webhooks. PayPal's older Instant Payment Notification (IPN) is a separate, form-encoded mechanism with its own verification handshake; advice written for one does not apply to the other.
What a PayPal webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
PAYPAL-TRANSMISSION-ID: 69cd13f0-d67a-11e5-baa3-778b53f4ae55
PAYPAL-TRANSMISSION-TIME: 2026-01-01T12:00:00Z
PAYPAL-TRANSMISSION-SIG: lmI95Jx3Y9nhR5SJWlHVIWpg4AgFk7n9bCHSRxbrd8A9zrhdu2rMyFrmz+Zjh3s3boXB07VXCXUZy/UFzUlnGJn0wDugt7FlSvdKeIJenLRemUxYCPVoEZzg9VFNqOa48gMkvF+XTpxBeUx/kWy6B5cp7GkT2+pOowfRK7OaynuxUoKW3JcMWw272VKjLTtTAShncla7tGF+55rxyt2KNZIIqxNMJ48RDZheGU5w1npu9dZHnPgTXB9iomeVRoD8O/jhRpnKsGrDschyNdkeh81BJJMH4Ctc6lnCCquoP/GzCzz33MMsNdid7vL/NIWaCsekQpW26FpWPi/tfj8nLA==
PAYPAL-CERT-URL: https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270
PAYPAL-AUTH-ALGO: SHA256withRSA
Content-Type: application/json
{
"id": "WH-58D329510W468432D-8HN650336L201105X",
"event_version": "1.0",
"create_time": "2026-01-01T12:00:00.000Z",
"resource_type": "capture",
"resource_version": "2.0",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"summary": "Payment completed for $ 30.00 USD",
"resource": {
"id": "42311647XV020574X",
"status": "COMPLETED",
"amount": {
"currency_code": "USD",
"value": "30.00"
},
"final_capture": true,
"seller_protection": {
"status": "ELIGIBLE"
},
"seller_receivable_breakdown": {
"gross_amount": {
"currency_code": "USD",
"value": "30.00"
},
"paypal_fee": {
"currency_code": "USD",
"value": "1.54"
},
"net_amount": {
"currency_code": "USD",
"value": "28.46"
}
},
"invoice_id": "ORDER-6735",
"create_time": "2026-01-01T11:59:58Z",
"update_time": "2026-01-01T11:59:58Z"
},
"links": [
{
"href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-58D329510W468432D-8HN650336L201105X",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-58D329510W468432D-8HN650336L201105X/resend",
"rel": "resend",
"method": "POST"
}
]
}
Amounts are strings with a decimal point ("30.00"), not integer minor units — the opposite of Stripe. resource_type and resource_version tell you how to read resource, whose shape differs completely between, say, a capture and a billing subscription.
Headers PayPal sends
| Header | Example | What it is |
|---|---|---|
PAYPAL-TRANSMISSION-ID | 69cd13f0-d67a-11e5-baa3-778b53f4ae55 | Unique id of this transmission. Stays the same across retries. |
PAYPAL-TRANSMISSION-TIME | 2026-01-01T12:00:00Z | When PayPal sent it. Part of the signed message. |
PAYPAL-TRANSMISSION-SIG | lmI95Jx3Y9nhR5SJWlHVIWpg4AgFk7n9bCHSRxbrd8A… | Base64 RSA signature. |
PAYPAL-CERT-URL | https://api.paypal.com/v1/notifications/cer… | Where to download the public certificate that verifies the signature. |
PAYPAL-AUTH-ALGO | SHA256withRSA | Signature algorithm. |
Content-Type | application/json | The body is JSON. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How PayPal signs a webhook
PayPal does not use a shared-secret HMAC. It signs with an RSA private key and tells you, in PAYPAL-CERT-URL, where to fetch the matching certificate. The signed message is four values joined with pipes: transmission id | transmission time | webhook id | CRC32 of the raw body (the CRC as a decimal number). The webhook id is the id of your webhook configuration from the developer dashboard — it is not in the request, which is what stops a signature made for someone else's listener from validating on yours.
There are two ways to check it. Verify locally — download the certificate, confirm it chains to PayPal and that the URL really is a PayPal host, then check the RSA signature over that message — or post the headers and the event to PayPal's verify-webhook-signature endpoint and read verification_status. The API call is simpler and costs a round trip per event; the example below uses it.
// headers: the incoming request headers (lower-cased), rawBody: body as received
async function verifyPayPal(headers, rawBody, webhookId, accessToken) {
const res = await fetch('https://api-m.paypal.com/v1/notifications/verify-webhook-signature', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + accessToken,
},
// Build the JSON by hand so webhook_event is the ORIGINAL bytes.
// JSON.parse + JSON.stringify can reorder or reformat and fail the check.
body: '{' +
'"auth_algo":' + JSON.stringify(headers['paypal-auth-algo']) + ',' +
'"cert_url":' + JSON.stringify(headers['paypal-cert-url']) + ',' +
'"transmission_id":' + JSON.stringify(headers['paypal-transmission-id']) + ',' +
'"transmission_sig":' + JSON.stringify(headers['paypal-transmission-sig']) + ',' +
'"transmission_time":' + JSON.stringify(headers['paypal-transmission-time']) + ',' +
'"webhook_id":' + JSON.stringify(webhookId) + ',' +
'"webhook_event":' + rawBody +
'}',
});
const data = await res.json();
return data.verification_status === 'SUCCESS';
}
Retries and what to return
Answer with a 2xx. If the listener returns anything else or does not respond, PayPal resends the notification — up to 25 times over the course of three days, per its documentation at the time of writing. The transmission id is unchanged across those attempts, and the event's own id (WH-…) is the natural de-duplication key.
Missed something anyway? Every event carries a resend link, and the dashboard's webhook events page lists past deliveries with their status, so a gap can be filled without waiting for another customer action.
Catch a real PayPal webhook
- Press Create webhook URL at the top of this page and copy the address.
- In the PayPal Developer Dashboard, open Apps & Credentials, stay on Sandbox, and select the app your payments run through.
- Under Webhooks, add a webhook and paste the address as the listener URL.
- Tick the event types you want — or all of them — and save.
- Complete a sandbox order, or use the dashboard's Webhooks simulator, and read the notification in the inbox above.
Events sent by the Webhooks simulator are mock events: they are not tied to your webhook id and cannot be verified with verify-webhook-signature. Trigger a real sandbox transaction when you are testing verification.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Verification fails after parsing. Both methods depend on the exact body bytes — the CRC32 locally,
webhook_eventvia the API. Keep the raw body. - Sandbox and live are separate worlds. Different apps, different webhook ids, different API hosts (
api-m.sandbox.paypal.comvsapi-m.paypal.com). - Do not trust
PAYPAL-CERT-URLblindly. If you verify locally, check that the host is a PayPal domain before downloading anything from it.
api-tester.com is independent and is not affiliated with PayPal. Providers change header names, limits and retry schedules; treat the official PayPal documentation as the source of truth.
Related examples and tools
Shopify Webhook Example
What Shopify posts when a store event fires — the topic and shop headers, the resource JSON, and a base64 HMAC that trips up people expecting hex.
Zapier Webhook Example
Zapier works in both directions.
Slack Webhook Example
The JSON Slack's Events API posts to your Request URL, the one-time url_verification handshake, and the v0 signature that covers a timestamp as well as the body.
Discord Webhook Example
A Discord webhook runs the other way from most: you send the request, and Discord posts it in a channel.
Twilio Webhook Example
Twilio's webhooks are form posts, not JSON, and the signature covers your URL as well as the parameters.
Idempotency
Why a retried notification must not capture or ship twice, and how idempotency keys solve it.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with a private key
References
- Official PayPal documentation — The provider's own guide to the requests described on this page.
- PayPal Developer: webhook event names — The list of event types by product.
- PayPal Developer: Webhooks Management API — The API for creating webhooks and verifying a signature.
These links leave api-tester.com, which is independent of the companies and projects they point to.
PayPal webhook FAQ
What does a PayPal webhook payload look like?
A JSON envelope with id, event_version, create_time, resource_type, resource_version, event_type, summary, resource and links. The resource object is the payment, refund, dispute or subscription the event is about.
What headers does PayPal add to a webhook request?
PAYPAL-TRANSMISSION-ID, PAYPAL-TRANSMISSION-TIME, PAYPAL-TRANSMISSION-SIG, PAYPAL-CERT-URL and PAYPAL-AUTH-ALGO, alongside Content-Type: application/json.
How does PayPal sign a webhook?
With an RSA signature (SHA256withRSA) over "transmission id|transmission time|webhook id|CRC32 of the body". You verify it with the certificate at PAYPAL-CERT-URL, or by calling PayPal's verify-webhook-signature API.
Does PayPal retry a failed webhook?
Yes. A non-2xx response is retried up to 25 times over three days according to PayPal's documentation at the time of writing, and any event can also be resent manually.
Is a PayPal webhook the same as IPN?
No. IPN is PayPal's older, form-encoded notification system with a postback verification step. REST webhooks are JSON, are configured per REST app and are verified by signature.