# PayPal

> Integrate PayPal payments with proper webhook verification, OAuth handling, and security validation for checkout flows and subscriptions.

- Skill: `modbender/paypal` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add modbender/paypal`
- Raw SKILL.md: https://api.skillmd.com/api/skills/modbender/paypal/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: modbender (https://skillmd.com/u/modbender)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/modbender/paypal

---


## When to Use

User needs to integrate PayPal REST API for payments, subscriptions, or payouts. Agent handles checkout flows, webhook verification, OAuth token management, and dispute workflows.

## Quick Reference

| Topic | File |
|-------|------|
| Code patterns | `patterns.md` |
| Webhook events | `webhooks.md` |

## Core Rules

### 1. Environment URLs are Different
- Sandbox: `api.sandbox.paypal.com`
- Production: `api.paypal.com`
- Ask which environment BEFORE generating code
- Credentials are environment-specific — never mix

### 2. OAuth Token Management
```javascript
// Token expires ~8 hours — handle refresh
const getToken = async () => {
  const res = await fetch('https://api.paypal.com/v1/oauth2/token', {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`,
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: 'grant_type=client_credentials'
  });
  return res.json(); // { access_token, expires_in }
};
```
Never hardcode tokens. Implement refresh logic.

### 3. Webhook Verification is Mandatory
PayPal webhooks MUST be verified via API call — not simple HMAC:
```javascript
// POST /v1/notifications/verify-webhook-signature
const verification = await fetch('https://api.paypal.com/v1/notifications/verify-webhook-signature', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    auth_algo: headers['paypal-auth-algo'],
    cert_url: headers['paypal-cert-url'],
    transmission_id: headers['paypal-transmission-id'],
    transmission_sig: headers['paypal-transmission-sig'],
    transmission_time: headers['paypal-transmission-time'],
    webhook_id: WEBHOOK_ID,
    webhook_event: body
  })
});
// verification_status === 'SUCCESS'
```

### 4. CAPTURE vs AUTHORIZE — Ask First
| Intent | Behavior |
|--------|----------|
| `CAPTURE` | Charges immediately on approval |
| `AUTHORIZE` | Reserves funds, capture later (up to 29 days) |

Changing intent after integration breaks the entire flow.

### 5. Server-Side Validation — Never Trust Client
```javascript
// After client approves, VERIFY on server before fulfillment
const order = await fetch(`https://api.paypal.com/v2/checkout/orders/${orderId}`, {
  headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.json());

// Validate ALL of these:
if (order.status !== 'APPROVED') throw new Error('Not approved');
if (order.purchase_units[0].amount.value !== expectedAmount) throw new Error('Amount mismatch');
if (order.purchase_units[0].amount.currency_code !== expectedCurrency) throw new Error('Currency mismatch');
if (order.purchase_units[0].payee.merchant_id !== YOUR_MERCHANT_ID) throw new Error('Wrong merchant');
```

### 6. Idempotency in Webhooks
PayPal may send the same webhook multiple times:
```javascript
const processed = await db.webhooks.findOne({ eventId: body.id });
if (processed) return res.status(200).send('Already processed');
await db.webhooks.insert({ eventId: body.id, processedAt: new Date() });
// Now process the event
```

### 7. Currency Decimal Rules
Some currencies have NO decimal places:
| Currency | Decimals | Example |
|----------|----------|---------|
| USD, EUR | 2 | "10.50" |
| JPY, TWD | 0 | "1050" (NOT "1050.00") |

Sending "10.50" for JPY = API error.

## Common Traps

- **IPN vs Webhooks** — IPN is legacy. Use Webhooks for new integrations. Never mix.
- **Order states** — CREATED → APPROVED → COMPLETED (or VOIDED). Handle ALL states, not just happy path.
- **Decimal confusion** — PayPal uses strings for amounts ("10.50"), not floats. Some currencies forbid decimals.
- **Sandbox rate limits** — Lower than production. Don't assume prod will fail the same way.
- **Payout vs Payment** — Payouts API is separate. Don't confuse sending money (Payouts) with receiving (Orders).

