Skip to main content
Business & GrowthPayment Services225 lines

Paypal

Accept payments with PayPal. Use this skill when the project needs to integrate

Quick Summary30 lines
You are a payments specialist who integrates PayPal into projects. PayPal offers
multiple integration paths — Smart Payment Buttons, hosted checkout, REST API for
orders and subscriptions — plus access to PayPal's massive user base and Venmo.

## Key Points

- Use Smart Payment Buttons — they handle UI, eligibility, and localization
- Always capture on the server, not the client — verify payment before granting access
- Use `custom_id` on orders and subscriptions to link to your users
- Test in sandbox with sandbox accounts before going live
- Handle refund webhooks — revoke access when refunded
- Support both PayPal and card payments for maximum conversion
- Use PayPal's subscription plans for recurring billing
- Granting access on client-side `onApprove` without server verification
- Building custom payment buttons instead of using Smart Payment Buttons
- Not handling subscription cancellation and payment failure webhooks
- Using authorize-only intent for digital goods (capture immediately instead)
- Not testing the full flow in sandbox — PayPal sandbox has unique quirks

## Quick Example

```html
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID&vault=true&intent=subscription"></script>
```

```bash
npm install @paypal/paypal-server-sdk
```
skilldb get payment-services-skills/PaypalFull skill: 225 lines
Paste into your CLAUDE.md or agent config

PayPal Payment Integration

You are a payments specialist who integrates PayPal into projects. PayPal offers multiple integration paths — Smart Payment Buttons, hosted checkout, REST API for orders and subscriptions — plus access to PayPal's massive user base and Venmo.

Core Philosophy

PayPal is a payment method, not just a processor

Unlike Stripe (which processes cards), PayPal is a payment method that users already have funded. Integration adds a trusted checkout option alongside card payments, increasing conversion for users who prefer PayPal or Venmo.

Capture, don't authorize-only

For most SaaS and digital products, capture payments immediately at checkout. Use authorize-and-capture only for physical goods where fulfillment may fail.

Smart Payment Buttons handle the UI

PayPal's JavaScript SDK renders contextual buttons (PayPal, Venmo, Pay Later) that adapt to the user's location, device, and eligibility. Don't build custom buttons.

Setup

Client-side (PayPal JS SDK)

<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID&vault=true&intent=subscription"></script>

Server-side

npm install @paypal/paypal-server-sdk
import { PayPalHttpClient, SandboxEnvironment, LiveEnvironment } from '@paypal/paypal-server-sdk';

const environment = process.env.NODE_ENV === 'production'
  ? new LiveEnvironment(process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET)
  : new SandboxEnvironment(process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET);

const client = new PayPalHttpClient(environment);

Key Techniques

Smart Payment Buttons (one-time payment)

<div id="paypal-button-container"></div>
<script>
paypal.Buttons({
  createOrder: async () => {
    const res = await fetch('/api/paypal/create-order', { method: 'POST' });
    const data = await res.json();
    return data.orderId;
  },
  onApprove: async (data) => {
    const res = await fetch('/api/paypal/capture-order', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ orderId: data.orderID }),
    });
    const result = await res.json();
    if (result.status === 'COMPLETED') {
      window.location.href = '/success';
    }
  },
}).render('#paypal-button-container');
</script>

Server-side order creation and capture

// Create order
export async function POST(req: Request) {
  const order = await client.execute({
    path: '/v2/checkout/orders',
    method: 'POST',
    body: {
      intent: 'CAPTURE',
      purchase_units: [{
        amount: {
          currency_code: 'USD',
          value: '29.00',
        },
        custom_id: userId, // Your reference
        description: 'Pro Plan - Monthly',
      }],
    },
  });

  return Response.json({ orderId: order.result.id });
}

// Capture order
export async function POST(req: Request) {
  const { orderId } = await req.json();
  const capture = await client.execute({
    path: `/v2/checkout/orders/${orderId}/capture`,
    method: 'POST',
  });

  if (capture.result.status === 'COMPLETED') {
    const userId = capture.result.purchase_units[0].custom_id;
    await grantAccess(userId);
  }

  return Response.json({ status: capture.result.status });
}

Subscriptions

<div id="paypal-button-container"></div>
<script>
paypal.Buttons({
  style: { shape: 'pill', label: 'subscribe' },
  createSubscription: (data, actions) => {
    return actions.subscription.create({
      plan_id: 'P-xxx', // Plan ID from PayPal dashboard
      custom_id: userId,
    });
  },
  onApprove: (data) => {
    fetch('/api/paypal/subscription-activated', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        subscriptionId: data.subscriptionID,
        userId: userId,
      }),
    });
  },
}).render('#paypal-button-container');
</script>

Subscription management (REST API)

// Get subscription details
const sub = await client.execute({
  path: `/v1/billing/subscriptions/${subscriptionId}`,
  method: 'GET',
});

// Cancel subscription
await client.execute({
  path: `/v1/billing/subscriptions/${subscriptionId}/cancel`,
  method: 'POST',
  body: { reason: 'Customer requested cancellation' },
});

// Suspend subscription
await client.execute({
  path: `/v1/billing/subscriptions/${subscriptionId}/suspend`,
  method: 'POST',
  body: { reason: 'Payment method issue' },
});

Webhook Processing

EventAction
PAYMENT.CAPTURE.COMPLETEDGrant access
BILLING.SUBSCRIPTION.ACTIVATEDSet up subscription
BILLING.SUBSCRIPTION.CANCELLEDRevoke access
BILLING.SUBSCRIPTION.SUSPENDEDRestrict access
BILLING.SUBSCRIPTION.PAYMENT.FAILEDNotify user
PAYMENT.CAPTURE.REFUNDEDRevoke access, adjust records
import crypto from 'crypto';

export async function POST(req: Request) {
  const body = await req.text();
  // Verify webhook signature (PayPal uses certificate-based verification)
  // In production, verify with PayPal's webhook verification API

  const event = JSON.parse(body);

  switch (event.event_type) {
    case 'PAYMENT.CAPTURE.COMPLETED':
      const customId = event.resource.custom_id;
      await grantAccess(customId);
      break;
    case 'BILLING.SUBSCRIPTION.CANCELLED':
      await revokeAccess(event.resource.custom_id);
      break;
    case 'BILLING.SUBSCRIPTION.PAYMENT.FAILED':
      await notifyPaymentFailed(event.resource.custom_id);
      break;
  }

  return new Response('OK');
}

Best Practices

  • Use Smart Payment Buttons — they handle UI, eligibility, and localization
  • Always capture on the server, not the client — verify payment before granting access
  • Use custom_id on orders and subscriptions to link to your users
  • Test in sandbox with sandbox accounts before going live
  • Handle refund webhooks — revoke access when refunded
  • Support both PayPal and card payments for maximum conversion
  • Use PayPal's subscription plans for recurring billing

Anti-Patterns

  • Granting access on client-side onApprove without server verification
  • Building custom payment buttons instead of using Smart Payment Buttons
  • Not handling subscription cancellation and payment failure webhooks
  • Using authorize-only intent for digital goods (capture immediately instead)
  • Not testing the full flow in sandbox — PayPal sandbox has unique quirks
  • Ignoring Venmo eligibility — it's available through Smart Payment Buttons

Install this skill directly: skilldb add payment-services-skills

Get CLI access →