PayPal Webhook: Example Payload, Headers and Signature

The notification PayPal posts when a payment event happens — its envelope, the five PAYPAL- headers, and the certificate-based signature that works differently from everyone else's HMAC.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by PayPal, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

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

HeaderExampleWhat it is
PAYPAL-TRANSMISSION-ID69cd13f0-d67a-11e5-baa3-778b53f4ae55Unique id of this transmission. Stays the same across retries.
PAYPAL-TRANSMISSION-TIME2026-01-01T12:00:00ZWhen PayPal sent it. Part of the signed message.
PAYPAL-TRANSMISSION-SIGlmI95Jx3Y9nhR5SJWlHVIWpg4AgFk7n9bCHSRxbrd8A…Base64 RSA signature.
PAYPAL-CERT-URLhttps://api.paypal.com/v1/notifications/cer…Where to download the public certificate that verifies the signature.
PAYPAL-AUTH-ALGOSHA256withRSASignature algorithm.
Content-Typeapplication/jsonThe 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

  1. Press Create webhook URL at the top of this page and copy the address.
  2. In the PayPal Developer Dashboard, open Apps & Credentials, stay on Sandbox, and select the app your payments run through.
  3. Under Webhooks, add a webhook and paste the address as the listener URL.
  4. Tick the event types you want — or all of them — and save.
  5. 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_event via the API. Keep the raw body.
  • Sandbox and live are separate worlds. Different apps, different webhook ids, different API hosts (api-m.sandbox.paypal.com vs api-m.paypal.com).
  • Do not trust PAYPAL-CERT-URL blindly. 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

Other webhooks signed with a private key

References

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.