paypal-webhooks
Receive and verify PayPal webhooks. Use when setting up PayPal webhook handlers, debugging certificate-based signature verification, or handling payment events like PAYMENT.CAPTURE.COMPLETED, PAYMENT.SALE.COMPLETED, BILLING.SUBSCRIPTION.CREATED, or CHECKOUT.ORDER.APPROVED.
What this skill does
# PayPal Webhooks
## When to Use This Skill
- Setting up PayPal webhook handlers
- Debugging PayPal signature verification failures (RSA-SHA256 with cert)
- Understanding PayPal event types like `PAYMENT.CAPTURE.COMPLETED`
- Handling payment, subscription, refund, or checkout events
- Choosing between PayPal's postback verify API and offline cert verification
## How PayPal Webhooks Differ From Most Providers
PayPal does **not** use HMAC with a shared secret. Instead, each webhook is
signed with PayPal's private key, and you verify it with the matching public
**certificate** delivered per request via the `paypal-cert-url` header. The
algorithm is **RSA-SHA256** ("SHA256withRSA").
Two valid verification paths:
1. **Postback (no crypto needed)** — POST the captured headers, your
`webhook_id`, and the raw `webhook_event` body to PayPal's
`/v1/notifications/verify-webhook-signature` endpoint. Requires an OAuth
access token. PayPal returns `{ "verification_status": "SUCCESS" }`.
2. **Offline self-verify (recommended for low-latency / no extra OAuth call)** —
Fetch the cert from `paypal-cert-url` (cache it; validate the host ends with
`.paypal.com`), build the message
`transmissionId|transmissionTime|webhookId|crc32(rawBody)`, and verify the
base64 signature against the cert's public key using RSA-SHA256.
The examples in this skill use the offline approach because it is testable
without OAuth and avoids an extra API call per webhook. The postback path is
documented in [references/verification.md](references/verification.md).
## Essential Code (USE THIS)
### Required Request Headers
| Header | Purpose |
|--------|---------|
| `paypal-transmission-id` | Unique webhook transmission ID |
| `paypal-transmission-time` | ISO 8601 timestamp of transmission |
| `paypal-transmission-sig` | Base64-encoded RSA-SHA256 signature |
| `paypal-cert-url` | URL of the public cert (must be a `*.paypal.com` host) |
| `paypal-auth-algo` | Signing algorithm, e.g. `SHA256withRSA` |
### Signed Message Format
```
<transmissionId>|<transmissionTime>|<webhookId>|<crc32(rawBody)>
```
`crc32(rawBody)` is the standard CRC-32 of the raw HTTP body as an **unsigned
decimal integer**. `webhookId` is the ID of the webhook registered in your
PayPal app (env var `PAYPAL_WEBHOOK_ID`).
### Express Webhook Handler
```javascript
const express = require('express');
const crypto = require('crypto');
const zlib = require('zlib');
const https = require('https');
const app = express();
const certCache = new Map();
function fetchCert(certUrl) {
// SECURITY: Only trust certs served from paypal.com
const host = new URL(certUrl).hostname;
if (host !== 'paypal.com' && !host.endsWith('.paypal.com')) {
return Promise.reject(new Error('Cert URL host is not paypal.com'));
}
if (certCache.has(certUrl)) return Promise.resolve(certCache.get(certUrl));
return new Promise((resolve, reject) => {
https.get(certUrl, (res) => {
let data = '';
res.on('data', (c) => (data += c));
res.on('end', () => { certCache.set(certUrl, data); resolve(data); });
}).on('error', reject);
});
}
async function verifyPayPalWebhook(headers, rawBody, webhookId) {
const transmissionId = headers['paypal-transmission-id'];
const transmissionTime = headers['paypal-transmission-time'];
const transmissionSig = headers['paypal-transmission-sig'];
const certUrl = headers['paypal-cert-url'];
if (!transmissionId || !transmissionTime || !transmissionSig || !certUrl) {
return false;
}
const crc = zlib.crc32(rawBody);
const message = `${transmissionId}|${transmissionTime}|${webhookId}|${crc}`;
const cert = await fetchCert(certUrl);
const verifier = crypto.createVerify('SHA256');
verifier.update(message);
verifier.end();
try {
return verifier.verify(cert, transmissionSig, 'base64');
} catch {
return false;
}
}
// CRITICAL: express.raw() — PayPal verification needs the raw body for CRC32
app.post('/webhooks/paypal',
express.raw({ type: 'application/json' }),
async (req, res) => {
const ok = await verifyPayPalWebhook(
req.headers,
req.body,
process.env.PAYPAL_WEBHOOK_ID
);
if (!ok) return res.status(400).send('Invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
switch (event.event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
console.log('Capture completed:', event.resource.id);
break;
case 'PAYMENT.CAPTURE.REFUNDED':
console.log('Refund issued:', event.resource.id);
break;
case 'BILLING.SUBSCRIPTION.CREATED':
console.log('Subscription created:', event.resource.id);
break;
case 'CHECKOUT.ORDER.APPROVED':
console.log('Order approved:', event.resource.id);
break;
default:
console.log('Unhandled event:', event.event_type);
}
res.json({ received: true });
}
);
```
### FastAPI Webhook Handler
```python
import os, zlib, base64, httpx
from urllib.parse import urlparse
from fastapi import FastAPI, Request, HTTPException
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
app = FastAPI()
_cert_cache: dict[str, bytes] = {}
def fetch_cert(cert_url: str) -> bytes:
host = urlparse(cert_url).hostname or ""
if host != "paypal.com" and not host.endswith(".paypal.com"):
raise ValueError("Cert URL host is not paypal.com")
if cert_url in _cert_cache:
return _cert_cache[cert_url]
pem = httpx.get(cert_url, timeout=10).content
_cert_cache[cert_url] = pem
return pem
def verify_paypal_webhook(headers, raw_body: bytes, webhook_id: str) -> bool:
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")
if not all([transmission_id, transmission_time, transmission_sig, cert_url]):
return False
crc = zlib.crc32(raw_body) & 0xFFFFFFFF
message = f"{transmission_id}|{transmission_time}|{webhook_id}|{crc}".encode()
cert_pem = fetch_cert(cert_url)
public_key = x509.load_pem_x509_certificate(cert_pem).public_key()
try:
public_key.verify(
base64.b64decode(transmission_sig),
message,
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except InvalidSignature:
return False
@app.post("/webhooks/paypal")
async def paypal_webhook(request: Request):
raw = await request.body()
if not verify_paypal_webhook(request.headers, raw, os.environ["PAYPAL_WEBHOOK_ID"]):
raise HTTPException(status_code=400, detail="Invalid signature")
event = await request.json()
# handle event.event_type ...
return {"received": True}
```
> **For complete working examples with tests**, see:
> - [examples/express/](examples/express/) — Full Express implementation
> - [examples/nextjs/](examples/nextjs/) — Next.js App Router implementation
> - [examples/fastapi/](examples/fastapi/) — Python FastAPI implementation
## Common Event Types
| Event | Description |
|-------|-------------|
| `PAYMENT.CAPTURE.COMPLETED` | A payment capture completed |
| `PAYMENT.CAPTURE.REFUNDED` | A capture was refunded |
| `PAYMENT.SALE.COMPLETED` | A sale completed (legacy Payments API) |
| `BILLING.SUBSCRIPTION.CREATED` | A subscription was created |
| `BILLING.SUBSCRIPTION.ACTIVATED` | A subscription was activated |
| `BILLING.SUBSCRIPTION.CANCELLED` | A subscription was cancelled |
| `CHECKOUT.ORDER.APPROVED` | A buyer approved a checkout order |
| `CHECKOUT.ORDER.COMPLETED` | A checkout order was completed |
| `CUSTOMER.DISPUTE.CREATED` | A dispute was opened |
> For the full list, see [PayPal Webhook Event Names](https://developer.paypal.com/api/rest/Related in Sales & CRM
process-mapper
IncludedUse when a BizOps lead, COO, or process-improvement owner needs to document an end-to-end business process (procurement, employee onboarding, incident handoff, customer-onboarding, claims adjudication) in BPMN-style notation, measure cycle times by stage, surface where work spends most of its time waiting vs. being worked, and quantify the gap between processing time and total elapsed time. Pairs Lean / Six Sigma / Theory-of-Constraints canon with deterministic stdlib-only Python tools to produce a process map, a ranked bottleneck list (with severity + root-cause hypothesis), and a cycle-time analysis (P50, P90, value-add ratio, Little's-Law throughput). Distinct from sales-pipeline, system-reliability (SLO), and strategic-OKR work — this is tactical process documentation for internal operations.
payment-integration
IncludedIntegrate payments with SePay (VietQR), Polar, Stripe, Paddle (MoR subscriptions), Creem.io (licensing). Checkout, webhooks, subscriptions, QR codes, multi-provider orders.
customer-success-manager
IncludedMonitors customer health, predicts churn risk, and identifies expansion opportunities using weighted scoring models for SaaS customer success
sales-engineer
IncludedAnalyzes RFP/RFI responses for coverage gaps, builds competitive feature comparison matrices, and plans proof-of-concept (POC) engagements for pre-sales engineering. Use when responding to RFPs, bids, or proposal requests; comparing product features against competitors; planning or scoring a customer POC or sales demo; preparing a technical proposal; or performing win/loss competitor analysis. Handles tasks described as 'RFP response', 'bid response', 'proposal response', 'competitor comparison', 'feature matrix', 'POC planning', 'sales demo prep', or 'pre-sales engineering'.
customer-success-manager
IncludedMonitors customer health, predicts churn risk, and identifies expansion opportunities using weighted scoring models for SaaS customer success
sales-engineer
IncludedAnalyzes RFP/RFI responses for coverage gaps, builds competitive feature comparison matrices, and plans proof-of-concept (POC) engagements for pre-sales engineering. Use when responding to RFPs, bids, or proposal requests; comparing product features against competitors; planning or scoring a customer POC or sales demo; preparing a technical proposal; or performing win/loss competitor analysis. Handles tasks described as 'RFP response', 'bid response', 'proposal response', 'competitor comparison', 'feature matrix', 'POC planning', 'sales demo prep', or 'pre-sales engineering'.