paypal-sandbox
Wraps PayPal Sandbox testing patterns: sandbox account creation (Business + Personal accounts in developer.paypal.com), the Orders v2 API (create / capture / refund), webhook event simulator (developer.paypal.com webhook simulator), sandbox-account-specific test cards, and the OAuth2 client-credentials flow for sandbox. Use when testing PayPal-integrated code. Composes payment-flow-states-reference.
paypal-sandbox
Overview
PayPal Sandbox is a parallel environment that mirrors the prod PayPal API. Per developer.paypal.com/tools/sandbox, sandbox accounts (Business + Personal) are created in the developer dashboard; tests use sandbox client credentials.
The current canonical API is Orders v2 (developer.paypal.com/docs/api/orders/v2); older Payments API (v1) is deprecated.
When to use
Authoring
Setup
Install
npm install @paypal/checkout-server-sdk
pip install paypalserversdkOAuth2 client credentials
import paypal from '@paypal/checkout-server-sdk';
const env = new paypal.core.SandboxEnvironment(
process.env.PAYPAL_SANDBOX_CLIENT_ID!,
process.env.PAYPAL_SANDBOX_SECRET!,
);
const client = new paypal.core.PayPalHttpClient(env);Create order
const request = new paypal.orders.OrdersCreateRequest();
request.requestBody({
intent: 'CAPTURE',
purchase_units: [{ amount: { currency_code: 'USD', value: '10.00' } }],
});
const order = await client.execute(request);
expect(order.result.status).toBe('CREATED');
expect(order.result.id).toBeTruthy();Capture order (after buyer approval)
const captureRequest = new paypal.orders.OrdersCaptureRequest(order.result.id);
captureRequest.requestBody({});
const capture = await client.execute(captureRequest);
expect(capture.result.status).toBe('COMPLETED');In test code, you need a sandbox buyer to approve the order via the PayPal checkout UI - for fully-automated tests, this requires Playwright + a sandbox Personal account login.
Sandbox test cards
Per developer.paypal.com/tools/sandbox/card-testing:
| Card | Behaviour |
|---|---|
| 4111 1111 1111 1111 | Visa Sandbox success |
| 5555 5555 5555 4444 | Mastercard success |
| 4032 0359 8001 0008 | Decline |
PayPal Sandbox is more PayPal-balance-oriented than card- oriented; sandbox buyers also have fake "PayPal balance."
Webhook simulator
Per developer.paypal.com/api/rest/webhooks/event-names: the developer dashboard exposes a Webhook Simulator that sends any event type to your registered URL.
For automated tests, use the simulator's API:
curl -X POST 'https://api-m.sandbox.paypal.com/v1/notifications/simulate-event' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{
"url": "https://example.com/webhook",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
...
}'Webhook signature verification
Per developer.paypal.com/api/rest/webhooks/rest: PayPal webhooks include PAYPAL-TRANSMISSION-SIG and related headers; verify via PayPal's verification endpoint or local SDK helper.
import { verifyWebhookSignature } from '@paypal/checkout-server-sdk';
const isValid = await verifyWebhookSignature({
authAlgo: headers['paypal-auth-algo'],
certUrl: headers['paypal-cert-url'],
transmissionId: headers['paypal-transmission-id'],
transmissionSig: headers['paypal-transmission-sig'],
transmissionTime: headers['paypal-transmission-time'],
webhookId: process.env.PAYPAL_WEBHOOK_ID!,
webhookEvent: notificationPayload,
});
expect(isValid).toBe(true);Running
npm testCI integration
jobs:
paypal-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- run: npm ci && npm test
env:
PAYPAL_SANDBOX_CLIENT_ID: ${{ secrets.PAYPAL_SANDBOX_CLIENT_ID }}
PAYPAL_SANDBOX_SECRET: ${{ secrets.PAYPAL_SANDBOX_SECRET }}Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Use live PayPal credentials in tests | Real money | Sandbox-only |
| Test with manual buyer approval | Slow; not CI-suitable | Playwright + sandbox buyer login |
| Skip webhook signature verification | Spoofable | Always verify |
| Hardcode sandbox account IDs | Fragile to account changes | Per-env IDs |
| Test only the API path | Real flow requires checkout UI | Playwright e2e |
| Legacy Payments v1 API | Deprecated | Migrate to Orders v2 |
Treat CREATED as final | Order needs capture | Test the full lifecycle |
| One-shot test for refunds | Refunds are async | Wait for webhook |