Skip to content

Verify PayPal webhooks

Verify that a webhook really came from PayPal before acting on it.

Identifier 'paypal'
Import import { verifyPayPal } from 'verihook/paypal'
Headers paypal-transmission-id
paypal-transmission-time
paypal-transmission-sig
paypal-cert-url
paypal-auth-algo
Signature RSA-SHA256 signature of <transmission id>|<time>|<webhook id>|<CRC32 of body>, checked with PayPal’s signing certificate.
Replay window None (the provider sends no timestamp)
result.eventType the body’s event_type, e.g. PAYMENT.CAPTURE.COMPLETED

The Webhook ID (not a secret): in the PayPal Developer Dashboard, open Apps & Credentials, select the app and find the ID in its webhooks list. Pass it as the secret or as { webhookId }. To avoid fetching the certificate, pass the PEM certificate as the secret and the ID as webhookId.

Store it in an environment variable (PAYPAL_WEBHOOK_ID below) and never commit it.

import { verifyPayPal } from 'verihook/paypal';
export async function POST(request: Request) {
const result = await verifyPayPal(request, process.env.PAYPAL_WEBHOOK_ID!);
if (!result.valid) {
console.warn(result.code, result.reason, result.hint);
return new Response('Invalid signature', { status: 401 });
}
console.log(result.eventType, result.event);
return Response.json({ received: true });
}

request can be a Fetch Request or { headers, body, url? } with the raw body. verifyWebhook('paypal', request, secret) from verihook does the same. Read why the raw body matters if verification fails behind a body parser.

app/api/webhooks/paypal/route.ts
import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('paypal', process.env.PAYPAL_WEBHOOK_ID!, async (payload, result) => {
// Runs only for a valid signature
console.log(result.eventType, result.event);
});

Each adapter responds 401 to an invalid signature, 413 to a body over maxBodySize (2 MB by default) and 200 to a duplicate when you pass a dedupeStore.

  • Verification fetches PayPal’s certificate from paypal-cert-url over HTTPS. Only PayPal API hosts are allowed, and certificates are cached in memory.
  • Sandbox and live webhooks have different IDs.

PayPal signs with its own private key, so verihook/testing can’t create PayPal webhooks. Use the provider’s sandbox or simulator, or mock verifyPayPal in unit tests.

Failed results carry a code and often a hint with the likely cause. See Troubleshooting for each error code.

PayPal’s webhook documentation