Gareth Wilson Gareth Wilson

Guide to PayPal Webhooks Features and Best Practices

Published


PayPal processes billions of transactions across more than 200 markets worldwide, making it one of the most widely integrated payment platforms on the web. Beyond its checkout and payments APIs, PayPal's webhook system enables developers to receive real-time notifications when events occur — from completed captures and refunds to subscription changes and disputes.

This guide covers everything you need to know about PayPal webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.

What are PayPal webhooks?

PayPal webhooks are HTTPS callbacks that deliver event notifications to your server whenever something happens on your PayPal account — a payment is captured, a subscription renews, a dispute is opened, and so on. They are the modern replacement for PayPal's legacy Instant Payment Notification (IPN) system, offering faster delivery, structured JSON payloads, RSA-SHA256 signature verification, and a more complete developer experience.

Webhooks in PayPal work at the application level. Each REST API app you create in the PayPal Developer Dashboard can register its own webhook endpoints, and only events associated with that specific app are delivered to its registered listeners. This means payments processed through one REST app won't generate webhook events for a different REST app, even if both apps are tied to the same PayPal account.

PayPal also supports webhook lookups, which tie a REST API app to a subject account. Events not tied to a specific REST app (such as payments initiated through PayPal's NVP/SOAP APIs or the PayPal.com UI) can be routed to a registered app via this lookup mechanism.

PayPal webhook features

FeatureDetails
Webhook configurationDeveloper Dashboard UI or Webhooks Management REST API
Signature algorithmRSA-SHA256 with certificate verification
Verification methodsSelf-cryptographic (offline) or Verify Webhook Signature API (postback)
Retry logicUp to 25 retries over 3 days with exponential backoff
Max webhooks per app10 webhook URLs
Payload formatJSON
Delivery protocolHTTPS (port 443 required)
Event filteringSubscribe to specific event types or use * for all events
Webhook simulatorBuilt-in simulator for generating mock test events
Manual retryResend failed events from the Developer Dashboard or via API
Browsable logWebhook Events Dashboard in Developer Dashboard

Webhook event categories

PayPal provides over 100 webhook event types organized across its product APIs. The major categories include:

Payments (Authorizations, Captures, Refunds): Events for the full payment lifecycle — PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED, PAYMENT.CAPTURE.REVERSED, PAYMENT.CAPTURE.DENIED, PAYMENT.AUTHORIZATION.VOIDED, PAYMENT.SALE.COMPLETED, and more. These correspond to both supported versions of the Payments API.

Orders: Events for the Orders API checkout flow — CHECKOUT.ORDER.APPROVED, CHECKOUT.ORDER.COMPLETED, CHECKOUT.PAYMENT-APPROVAL.REVERSED, and others.

Subscriptions (Billing): Events for recurring billing — BILLING.SUBSCRIPTION.CREATED, BILLING.SUBSCRIPTION.ACTIVATED, BILLING.SUBSCRIPTION.CANCELLED, BILLING.SUBSCRIPTION.EXPIRED, BILLING.SUBSCRIPTION.PAYMENT.FAILED. Note that the older Billing Agreements webhooks are deprecated alongside the Billing Agreements REST API.

Disputes: Events for customer disputes and chargebacks — CUSTOMER.DISPUTE.CREATED, CUSTOMER.DISPUTE.RESOLVED, CUSTOMER.DISPUTE.UPDATED. Dispute reasons include merchandise not received, not as described, unauthorized transactions, credit not processed, duplicate transactions, and incorrect amounts.

Payouts: Events for batch and individual payout operations.

Vault (Payment Method Tokens): Events for saved payment methods in PayPal's digital vault.

Additional categories include Invoicing, Catalog Products, Identity, Partner Referrals, and Transaction Search.

Webhook payload structure

When PayPal sends a webhook notification, it delivers a JSON payload with the following general structure:

{
  "id": "WH-7YX49823S2290830K-0JE13296W68552352",
  "event_version": "1.0",
  "create_time": "2025-05-16T05:19:19.355Z",
  "resource_type": "capture",
  "resource_version": "2.0",
  "event_type": "PAYMENT.CAPTURE.COMPLETED",
  "summary": "Payment completed for $ 50.00 USD",
  "resource": {
    "id": "2GG279541U471931P",
    "amount": {
      "currency_code": "USD",
      "value": "50.00"
    },
    "final_capture": true,
    "seller_protection": {
      "status": "ELIGIBLE",
      "dispute_categories": [
        "ITEM_NOT_RECEIVED",
        "UNAUTHORIZED_TRANSACTION"
      ]
    },
    "seller_receivable_breakdown": {
      "gross_amount": {
        "currency_code": "USD",
        "value": "50.00"
      },
      "paypal_fee": {
        "currency_code": "USD",
        "value": "1.76"
      },
      "net_amount": {
        "currency_code": "USD",
        "value": "48.24"
      }
    },
    "status": "COMPLETED",
    "supplementary_data": {
      "related_ids": {
        "order_id": "5O190127TN364715T"
      }
    },
    "payee": {
      "email_address": "merchant@example.com",
      "merchant_id": "X5XAHHCG636FA"
    },
    "create_time": "2025-05-16T05:19:19Z",
    "update_time": "2025-05-16T05:19:19Z",
    "links": [
      {
        "href": "https://api.paypal.com/v2/payments/captures/2GG279541U471931P",
        "rel": "self",
        "method": "GET"
      },
      {
        "href": "https://api.paypal.com/v2/payments/captures/2GG279541U471931P/refund",
        "rel": "refund",
        "method": "POST"
      },
      {
        "href": "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T",
        "rel": "up",
        "method": "GET"
      }
    ]
  },
  "links": [
    {
      "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-7YX49823S2290830K-0JE13296W68552352",
      "rel": "self",
      "method": "GET"
    },
    {
      "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-7YX49823S2290830K-0JE13296W68552352/resend",
      "rel": "resend",
      "method": "POST"
    }
  ]
}

Key payload fields

FieldDescription
idUnique webhook event notification ID
event_versionVersion of the event schema
create_timeTimestamp of when the event occurred
resource_typeType of resource (e.g., capture, sale, refund, dispute)
event_typeThe event name (e.g., PAYMENT.CAPTURE.COMPLETED)
summaryHuman-readable description of the event
resourceThe actual resource object with full event details
resource.amountPayment amount with currency code
resource.seller_receivable_breakdownFee breakdown including PayPal fees and net amount
resource.supplementary_data.related_idsRelated resource IDs (e.g., the originating order ID)
resource.linksHATEOAS links for related API operations (self, refund, etc.)
linksLinks to manage the webhook event itself (view details, resend)

PayPal webhook payloads are typically compact — under 10KB — containing event metadata and summary information rather than complete resource details. For full transaction data, use the HATEOAS links in the resource.links array to fetch the complete resource via the PayPal REST API.

Security with RSA-SHA256 signatures

PayPal signs every webhook notification using RSA-SHA256 with certificate-based verification. When PayPal sends a webhook, it includes several verification headers:

HeaderDescription
PAYPAL-TRANSMISSION-IDUnique UUID for the transmission
PAYPAL-TRANSMISSION-TIMEISO 8601 timestamp of when PayPal initiated the transmission
PAYPAL-TRANSMISSION-SIGBase64-encoded RSA-SHA256 signature
PAYPAL-CERT-URLURL to the certificate corresponding to the private key used to sign
PAYPAL-AUTH-ALGOAlgorithm used (typically SHA256withRSA)

The signature is computed over a concatenated string of the transmission ID, timestamp, your webhook ID, and a CRC32 checksum of the request body, separated by pipe characters:

<transmission_id>|<transmission_time>|<webhook_id>|<crc32_of_body>

PayPal offers two verification approaches:

Self-cryptographic (offline) verification — Download the certificate from the PAYPAL-CERT-URL, extract the public key, and verify the signature locally. This is faster and avoids an extra API dependency, but requires you to manage certificate fetching and caching.

Verify Webhook Signature API (postback) — Send the transmission details back to PayPal's POST /v1/notifications/verify-webhook-signature endpoint and let PayPal perform the verification. This is simpler to implement but adds latency and creates a dependency on PayPal's API availability. Note that postback verification does not work with mock/simulator events.

Setting up PayPal webhooks

Via the Developer Dashboard

  1. Log in to the PayPal Developer Dashboard
  2. Navigate to Apps & Credentials
  3. Select your application (or create a new one)
  4. Scroll down to the Webhooks section
  5. Click Add Webhook
  6. Enter your HTTPS listener URL (must be port 443)
  7. Select the event types you want to subscribe to, or choose all events with *
  8. Click Save

Record the Webhook ID that PayPal assigns — you'll need it for signature verification.

Via the Webhooks Management API

You can also manage webhooks programmatically using the REST API. The key endpoints are:

Create a webhook:

curl -X POST https://api-m.paypal.com/v1/notifications/webhooks \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-endpoint.com/webhooks/paypal",
    "event_types": [
      { "name": "PAYMENT.CAPTURE.COMPLETED" },
      { "name": "PAYMENT.CAPTURE.REFUNDED" },
      { "name": "CUSTOMER.DISPUTE.CREATED" }
    ]
  }'

Best practices when working with PayPal webhooks

Verifying webhook signatures

Always verify the RSA-SHA256 signature on incoming webhooks to ensure they genuinely originated from PayPal.

Node.js

const crypto = require('crypto');
const crc32 = require('crc-32');
const https = require('https');
const express = require('express');
const app = express();

// Capture raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

async function verifyPayPalWebhook(req, webhookId) {
  const transmissionId = req.headers['paypal-transmission-id'];
  const transmissionTime = req.headers['paypal-transmission-time'];
  const transmissionSig = req.headers['paypal-transmission-sig'];
  const certUrl = req.headers['paypal-cert-url'];
  const authAlgo = req.headers['paypal-auth-algo'];

  // Compute CRC32 of the raw body
  const crc = crc32.str(req.rawBody) >>> 0; // unsigned

  // Construct the expected message
  const expectedMessage = `${transmissionId}|${transmissionTime}|${webhookId}|${crc}`;

  // Fetch PayPal's certificate (cache this in production)
  const cert = await fetchCertificate(certUrl);

  // Verify the signature
  const verifier = crypto.createVerify('SHA256');
  verifier.update(expectedMessage);
  return verifier.verify(cert, transmissionSig, 'base64');
}

app.post('/webhooks/paypal', async (req, res) => {
  const isValid = await verifyPayPalWebhook(req, process.env.PAYPAL_WEBHOOK_ID);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge immediately, process asynchronously
  res.status(200).send('OK');

  // Process the event
  processEvent(req.body);
});

Python

import hashlib
import base64
import binascii
import requests
from flask import Flask, request, abort
from OpenSSL import crypto

app = Flask(__name__)

def verify_paypal_webhook(headers, body, webhook_id):
    transmission_id = headers.get('PAYPAL-TRANSMISSION-ID')
    transmission_time = headers.get('PAYPAL-TRANSMISSION-TIME')
    transmission_sig = headers.get('PAYPAL-TRANSMISSION-SIG')
    cert_url = headers.get('PAYPAL-CERT-URL')

    # Compute CRC32 of the raw body
    crc = binascii.crc32(body.encode('utf-8')) & 0xFFFFFFFF

    # Construct the expected message
    expected_message = f"{transmission_id}|{transmission_time}|{webhook_id}|{crc}"

    # Fetch and parse the certificate (cache in production)
    cert_pem = requests.get(cert_url).content
    x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)

    # Verify the signature
    try:
        crypto.verify(
            x509,
            base64.b64decode(transmission_sig),
            expected_message.encode('utf-8'),
            'sha256'
        )
        return True
    except crypto.Error:
        return False

@app.route('/webhooks/paypal', methods=['POST'])
def handle_paypal_webhook():
    if not verify_paypal_webhook(
        request.headers,
        request.get_data(as_text=True),
        os.environ['PAYPAL_WEBHOOK_ID']
    ):
        abort(401)

    # Acknowledge immediately
    return 'OK', 200

Respond quickly and process asynchronously

Your webhook listener must respond with an HTTP 2xx status code promptly. If PayPal doesn't receive a response — or receives a non-2xx status — it will retry delivery up to 25 times over 3 days. Acknowledge the webhook immediately, then queue the event for asynchronous processing.

Implement idempotent processing

PayPal may deliver the same event multiple times due to retries. Use the webhook event id field to implement idempotent processing and prevent duplicate handling:

async function processPayPalEvent(event) {
  const eventId = event.id;

  // Check if already processed
  const exists = await redis.get(`processed:${eventId}`);
  if (exists) {
    console.log(`Event ${eventId} already processed, skipping`);
    return;
  }

  // Process the event
  await handleEvent(event);

  // Mark as processed with a TTL
  await redis.setex(`processed:${eventId}`, 86400 * 7, '1'); // 7-day TTL
}

Don't rely solely on webhook data for critical operations

PayPal can deliver events out of order due to network conditions and retry logic. For critical operations like order fulfilment, always fetch the latest resource state from PayPal's REST API using the resource ID or HATEOAS links from the webhook payload.

Use separate sandbox and production webhooks

Always maintain separate webhook URLs and webhook IDs for sandbox (testing) and production environments. This prevents test data from mixing with production data and allows you to test webhook changes safely without affecting live transactions.

PayPal webhook limitations and pain points

Unreliable webhook delivery in sandbox and production

The Problem: PayPal webhook delivery has well-documented reliability issues in both sandbox and production environments. Developers frequently report webhooks not firing at all, critical events like PAYMENT.CAPTURE.COMPLETED never being generated, and the Developer Dashboard showing events stuck in "pending" status even after the listener returns a 200 response.

Why It Happens: PayPal's webhook delivery infrastructure can experience queue backlogs and intermittent failures. In sandbox, the issues are more frequent — PayPal has acknowledged delivery delays and missing events in its developer community forums. In production, some developers report randomly dropped webhooks on integrations that previously worked reliably.

Workarounds:

  • Implement a polling fallback that periodically queries PayPal's REST APIs (e.g., Orders API, Transactions API) to catch events that webhooks may have missed
  • For critical flows like order fulfilment, don't rely solely on webhooks — use them as a fast-path notification and verify state via API calls
  • Monitor your webhook receipt rate and alert on gaps

How Hookdeck Can Help: Hookdeck sits between PayPal and your application, providing delivery guarantees with automatic retries, a dead letter queue for failed events, and full visibility into what was received and what was delivered. If your endpoint is temporarily unavailable, Hookdeck queues events and retries them on a configurable schedule, so you never lose a webhook even if PayPal's own retry window expires.

Fragile signature verification

The Problem: PayPal's signature verification is notoriously fragile. The signed string uses a CRC32 checksum of the request body, and any deviation in the raw body (such as parsing to an object and re-serializing back to JSON) will produce a different CRC32 and fail verification. Many web frameworks automatically parse the body before your handler sees it, making it easy to lose the original byte-for-byte content.

Why It Happens: The verification scheme relies on computing CRC32 over the exact raw bytes of the request body. CRC32 is sensitive to any change like whitespace, key ordering, and encoding differences. Additionally, the self-verification approach requires downloading and caching PayPal's signing certificate, adding implementation complexity.

Workarounds:

  • Capture the raw request body before any JSON parsing
  • Cache PayPal's signing certificates to avoid repeated HTTPS fetches
  • Consider using the Verify Webhook Signature API (postback) for simpler implementation, accepting the trade-off of additional latency and API dependency

How Hookdeck Can Help: Hookdeck handles webhook signature verification at the infrastructure level with built-in PayPal source verification. It verifies the original signature from PayPal and then resigns requests before forwarding to your endpoint, so you can either verify Hookdeck's simpler HMAC signature or rely on Hookdeck's verification entirely.

Maximum of 10 webhooks per application

The Problem: PayPal enforces a hard limit of 10 webhook URLs per REST API application. For complex applications that need to route different event types to different services or microservices, this limit can become a constraint.

Why It Happens: PayPal's webhook system is designed around the assumption that a single endpoint handles all event types for an application. The 10-URL limit is an architectural constraint of their platform.

Workarounds:

  • Design a single webhook receiver that routes events to different internal services based on event type
  • Use the wildcard (*) subscription on one URL to receive all events and handle routing in your application layer
  • Create additional REST API apps if you need more webhook URLs, though this adds management complexity

How Hookdeck Can Help: Hookdeck accepts webhooks on a single URL and can route, filter, and fan out events to multiple destinations based on event type or payload content. You use one webhook URL from your PayPal quota, and Hookdeck distributes events to as many downstream services as you need.

No historical event backfill

The Problem: PayPal webhooks only notify you about events that occur after you configure the webhook subscription. There is no way to receive webhook notifications for historical events e.g. if you set up a webhook today, you won't receive events for yesterday's transactions.

Why It Happens: PayPal's webhook system is designed as a forward-looking notification mechanism, not a historical event replay system. Events are generated in real time and delivered to currently registered listeners.

Workarounds:

  • Use PayPal's REST APIs (Transactions API, Orders API, Subscriptions API) to query and backfill historical data when setting up a new integration
  • Register webhook subscriptions as early as possible in your integration timeline
  • Maintain your own event store so you can replay events if needed

How Hookdeck Can Help: Hookdeck stores all received webhook events, enabling you to replay historical events to new endpoints or reprocess events after deploying fixes. While this doesn't solve the initial backfill from PayPal, it ensures you never lose events going forward.

Out-of-order event delivery

The Problem: PayPal does not guarantee the order of webhook delivery. Events may arrive in a different sequence than they occurred, for example, a PAYMENT.CAPTURE.REFUNDED event could arrive before the corresponding PAYMENT.CAPTURE.COMPLETED event, especially during retries.

Why It Happens: Network conditions, retry logic, and PayPal's internal event processing pipeline can all cause events to be delivered out of sequence. Retried events are particularly likely to arrive out of order relative to newer events.

Workarounds:

  • Use the create_time and update_time fields in the resource object to determine actual event ordering
  • Design your webhook handlers to be state-aware — fetch the latest resource state from PayPal's API before taking action
  • Implement event buffering or ordering logic for workflows that require strict sequencing

How Hookdeck Can Help: Hookdeck's event ordering capabilities can buffer and reorder events before delivering them to your endpoint, ensuring your application receives events in the correct sequence without building custom ordering logic.

Webhook simulator doesn't support signature verification

The Problem: PayPal's built-in webhook simulator generates mock events for testing, but these mock events cannot be verified using the Verify Webhook Signature API (postback). This creates a gap between your testing and production environments — you can't fully validate your signature verification logic with simulated events.

Why It Happens: Mock events generated by the simulator are not associated with a real REST API app or webhook subscription, so PayPal's verification endpoint has no context to validate them against.

Workarounds:

  • Test signature verification with real sandbox transactions rather than the simulator
  • Use the self-cryptographic (offline) verification method, which does work with simulator events
  • Maintain separate code paths for development (skip verification) and production (enforce verification), but be careful not to ship the development path

How Hookdeck Can Help: Hookdeck's CLI and Console allow you to capture, inspect, and replay real webhook events during development. You can test with actual PayPal sandbox events routed through Hookdeck to your localhost, getting full signature verification without relying on the simulator.

Local development requires tunneling

The Problem: PayPal needs to send webhooks to a publicly accessible HTTPS URL on port 443. During local development, your machine is behind firewalls and NAT, so PayPal can't reach your localhost endpoint directly.

Why It Happens: Webhooks fundamentally require the provider to initiate an HTTP connection to your server. Local development environments are not publicly routable by design.

Workarounds:

  • Use tunneling tools like Hookdeck CLI to expose your local server
  • Use PayPal's webhook simulator for basic payload testing (though with the verification limitations noted above)
  • Deploy to a staging environment for integration testing

How Hookdeck Can Help: The Hookdeck CLI creates a secure tunnel from your local development environment to Hookdeck's infrastructure, giving you a stable public URL to register with PayPal. Events are proxied to your localhost with full visibility in Hookdeck's dashboard, and you can replay events without triggering new PayPal transactions.

Subscription billing floods

The Problem: If you have a large number of subscriptions that renew simultaneously, for example, on the first of the month — PayPal can deliver a flood of webhook notifications in a very short window, potentially overwhelming your endpoint.

Why It Happens: PayPal generates and delivers webhook events as subscriptions are processed. When many subscriptions share the same billing cycle, the events are generated in bulk and delivered in rapid succession.

Workarounds:

  • Implement rate limiting and queuing in your webhook handler
  • Use a message queue (Hookdeck, SQS, RabbitMQ, Redis) between your webhook endpoint and your processing logic
  • Scale your webhook receiver horizontally to handle burst traffic
  • Stagger subscription billing dates if possible

How Hookdeck Can Help: Hookdeck provides built-in rate limiting and queuing that absorbs traffic spikes and delivers events to your endpoint at a controlled rate. It acts as a buffer between PayPal's burst delivery and your application's processing capacity, preventing overload without losing events.

Opaque retry schedule

The Problem: PayPal retries failed webhook deliveries up to 25 times over 3 days using exponential backoff, but does not publicly document the exact interval between each attempt. Developers cannot predict when the next retry will occur or plan their recovery windows accordingly.

Why It Happens: PayPal's retry schedule is an internal implementation detail that they don't expose. The documentation states "exponential backoff" without specifying the base interval, multiplier, or jitter strategy.

Workarounds:

  • Design your system assuming retries could arrive at any point within the 3-day window
  • Implement your own reconciliation process that runs independently of webhooks
  • Monitor the Webhook Events Dashboard for failed deliveries and manually resend when needed

How Hookdeck Can Help: Hookdeck provides fully configurable retry policies with transparent scheduling. You can set the retry count, interval, backoff strategy, and maximum retry window, giving you complete control over delivery behavior rather than depending on PayPal's opaque retry logic.

Testing PayPal webhooks

Use the webhook simulator

PayPal's Developer Dashboard includes a webhook simulator that generates mock events for testing. To use it:

  1. Navigate to the Webhooks Simulator in your Developer Dashboard
  2. Select the event type you want to simulate
  3. Click Send Test

Be aware that simulator events have limitations — they don't support postback signature verification and may differ slightly from real event payloads.

Use a request inspector

Before building your handler, inspect real PayPal payloads to understand their structure:

  1. Create a temporary webhook URL using Hookdeck's Console
  2. Register it as your webhook endpoint in the PayPal Developer Dashboard
  3. Trigger a real sandbox transaction (create and capture an order, initiate a refund, etc.)
  4. Inspect the payload structure, headers, and signature details in the console

Validate with real sandbox transactions

The most reliable way to test your integration is with actual sandbox transactions. Create sandbox buyer and seller accounts, process real transactions through PayPal's sandbox environment, and verify that your webhook handler correctly receives, verifies, and processes the resulting events.

Conclusion

PayPal webhooks provide a flexible event-driven foundation for integrating payment flows into your application. The rich set of event types covering payments, subscriptions, disputes, and more enables developers to build responsive systems that react to transaction lifecycle changes in real time. RSA-SHA256 signatures, a comprehensive management API, and the webhook events dashboard round out the feature set.

However, well-documented reliability issues in both sandbox and production, fragile signature verification, a limited webhook URL quota, and an opaque retry schedule mean that production deployments require careful engineering. Implementing asynchronous processing, idempotent handlers, and API-based reconciliation will address most common issues.

For teams processing moderate transaction volumes with straightforward routing needs, PayPal's built-in webhook system combined with proper verification and error handling works well. For high-volume payment platforms, complex multi-service architectures, or mission-critical financial workflows where every event matters, webhook infrastructure like Hookdeck can address PayPal's limitations — providing reliable delivery guarantees, configurable retries, payload transformation, and comprehensive observability without modifying your PayPal configuration.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.