Skip to main content
npm install @paykit-sdk/paypal

Setup

import { PayKit } from '@paykit-sdk/core';
import { paypal } from '@paykit-sdk/paypal';

export const paykit = new PayKit(paypal());
Required env vars:
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_SANDBOX=true
PAYPAL_WEBHOOK_SECRET=...

How it works

In PayPal, an Order is both the checkout and the payment. createCheckout and createPayment both create a PayPal order with intent: CAPTURE. The customer approves the order via PayPal’s UI (redirect or JS SDK), then you capture it with capturePayment(orderId). PAYPAL_WEBHOOK_SECRET is the Webhook ID from your PayPal dashboard — not a signing secret. PayPal verifies webhooks server-side by calling its own verification API using headers sent with the request.
PayPal does not support standalone customer management. createCustomer, updateCustomer, deleteCustomer, and retrieveCustomer all throw ProviderNotSupportedError. Use payer information within orders instead.
PayPal metadata is stored as the order’s customId (JSON-stringified). The combined length of all metadata keys and values must not exceed 127 characters after JSON serialization.

Webhooks

Enable these events in your PayPal dashboard:
  • CHECKOUT.ORDER.APPROVED
  • CHECKOUT.ORDER.COMPLETED
  • PAYMENT.CAPTURE.COMPLETED
  • PAYMENT.CAPTURE.REFUNDED
  • BILLING.SUBSCRIPTION.CREATED
  • BILLING.SUBSCRIPTION.UPDATED
  • BILLING.SUBSCRIPTION.ACTIVATED
  • BILLING.SUBSCRIPTION.SUSPENDED
  • BILLING.SUBSCRIPTION.CANCELLED
  • BILLING.SUBSCRIPTION.EXPIRED
const webhook = paykit.webhooks
  .setup({ webhookSecret: process.env.PAYPAL_WEBHOOK_SECRET! }) // Webhook ID from dashboard
  .on('payment.created', async event => {
    /* CHECKOUT.ORDER.APPROVED */
  })
  .on('payment.succeeded', async event => {
    /* CHECKOUT.ORDER.COMPLETED or PAYMENT.CAPTURE.COMPLETED */
  })
  .on('refund.created', async event => {
    /* PAYMENT.CAPTURE.REFUNDED */
  })
  .on('subscription.created', async event => {
    /* BILLING.SUBSCRIPTION.CREATED */
  })
  .on('subscription.updated', async event => {
    /* BILLING.SUBSCRIPTION.UPDATED / ACTIVATED / SUSPENDED */
  })
  .on('subscription.canceled', async event => {
    /* BILLING.SUBSCRIPTION.CANCELLED / EXPIRED */
  });

await webhook.handle({
  body: rawBody,
  headersAsObject: Object.fromEntries(request.headers),
  fullUrl: request.url,
});

Raw PayPal events

Opt into any native PayPal event — typed against the full PayPal event catalog:
paykit.webhooks
  .setup({ webhookSecret: process.env.PAYPAL_WEBHOOK_SECRET! })
  .on('paypal.PAYMENT.CAPTURE.COMPLETED', async event => {
    // event.data is typed as PayPalWebhookEvent<'PAYMENT.CAPTURE.COMPLETED'>
  })
  .on('paypal.BILLING.SUBSCRIPTION.PAYMENT.FAILED', async event => {
    // event.data is typed as PayPalWebhookEvent<'BILLING.SUBSCRIPTION.PAYMENT.FAILED', PayPalSubscription>
  });
All available raw events:
PayPal eventPayKit event emitted
paypal.CHECKOUT.ORDER.APPROVEDpayment.created
paypal.CHECKOUT.ORDER.COMPLETEDpayment.succeeded
paypal.PAYMENT.CAPTURE.COMPLETEDpayment.succeeded
paypal.PAYMENT.CAPTURE.REFUNDEDrefund.created
paypal.BILLING.SUBSCRIPTION.CREATEDsubscription.created
paypal.BILLING.SUBSCRIPTION.UPDATEDsubscription.updated
paypal.BILLING.SUBSCRIPTION.ACTIVATEDsubscription.updated
paypal.BILLING.SUBSCRIPTION.SUSPENDEDsubscription.updated
paypal.BILLING.SUBSCRIPTION.CANCELLEDsubscription.canceled
paypal.BILLING.SUBSCRIPTION.EXPIREDsubscription.canceled
paypal.PAYMENT.AUTHORIZATION.CREATED(raw only)
paypal.PAYMENT.AUTHORIZATION.VOIDED(raw only)
paypal.PAYMENT.CAPTURE.DECLINED(raw only)
paypal.PAYMENT.CAPTURE.PENDING(raw only)
paypal.PAYMENT.CAPTURE.REVERSED(raw only)
paypal.PAYMENT.REFUND.PENDING(raw only)
paypal.PAYMENT.REFUND.FAILED(raw only)
paypal.BILLING.SUBSCRIPTION.PAYMENT.FAILED(raw only)
paypal.CUSTOMER.DISPUTE.CREATED(raw only)
paypal.CUSTOMER.DISPUTE.RESOLVED(raw only)
paypal.CUSTOMER.DISPUTE.UPDATED(raw only)

provider_metadata

createCheckout requires currency, amount, and itemName in provider_metadata because PayPal orders need explicit line-item data:
// checkout.provider_metadata — currency, amount, and itemName are required
await paykit.checkouts.create({
  customer: { email: 'user@example.com' },
  item_id: 'sku_abc123',
  session_type: 'one_time',
  quantity: 2,
  success_url: 'https://example.com/success',
  cancel_url: 'https://example.com/cancel',
  provider_metadata: {
    currency: 'USD',      // required
    amount: '29.00',      // required — total order amount as string
    itemName: 'T-Shirt',  // required — display name shown in PayPal UI
  },
});

// refunds — amount is optional (defaults to full capture amount)
await paykit.refunds.create({
  payment_id: 'order_abc123',
  amount: 1000, // in currency units (e.g. cents)
});