PayPal
PayPal uses Orders v2 with capture: the library creates an order, you redirect the user to PayPal's approval page and, on return, the library captures the payment. The source of truth is the webhook, though the browser return also confirms the capture.
Configuration
import { PayPalGateway } from '@kaizen/payments-gateway'
PayPalGateway.register({
client_id: process.env.PAYPAL_CLIENT_ID!,
client_secret: process.env.PAYPAL_CLIENT_SECRET!,
webhook_id: process.env.PAYPAL_WEBHOOK_ID!,
environment: 'sandbox', // 'sandbox' | 'production'
public_base_url: process.env.PAYPAL_PUBLIC_BASE_URL!,
})| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | ✅ | Client ID of your PayPal REST app |
client_secret | string | ✅ | Client Secret of your PayPal REST app |
webhook_id | string | ✅ | Webhook ID, to verify the signature of notifications |
environment | 'sandbox' | 'production' | ✅ | Selects the PayPal API endpoint |
public_base_url | string | ✅ | Public URL of your API; PayPal returns the user to …/payments/paypal/return |
Environment variables
You provide the values from your own environment and pass them to PayPalGateway.register():
PAYPAL_CLIENT_ID=AeA1QIZ...
PAYPAL_CLIENT_SECRET=EGnHDxD...
PAYPAL_WEBHOOK_ID=8SB63863JN...
PAYPAL_ENVIRONMENT=sandbox # 'sandbox' | 'production'
PAYPAL_PUBLIC_BASE_URL=https://your-domain.com| Variable | Field | Required | Description |
|---|---|---|---|
PAYPAL_CLIENT_ID | client_id | ✅ | REST app Client ID |
PAYPAL_CLIENT_SECRET | client_secret | ✅ | REST app Client Secret |
PAYPAL_WEBHOOK_ID | webhook_id | ✅ | Webhook ID to verify the signature |
PAYPAL_ENVIRONMENT | environment | ✅ | sandbox or production (picks the endpoint) |
PAYPAL_PUBLIC_BASE_URL | public_base_url | ✅ | Public URL of your API for the browser return |
Create a charge
curl -X POST http://localhost:3000/payments/charge \
-H 'content-type: application/json' \
-d '{
"gateway": "paypal",
"amount": 4900,
"currency": "EUR",
"reference": "order-001",
"redirect": {
"success_url": "http://localhost:3000/ok",
"cancel_url": "http://localhost:3000/ko"
}
}'redirect.success_urlandredirect.cancel_urlare mandatory for PayPal. Without them →400.amountis in the currency's smallest unit (4900 = €49.00).- The response has a
redirect_urlwith PayPal's approval page: send the user there.
The response has the same shape as every other gateway:
{
"payment_id": "b9c1…",
"gateway": "paypal",
"status": "pending",
"redirect_url": "https://www.sandbox.paypal.com/checkoutnow?token=…"
}How the user pays
- Redirect the user to
redirect_url(PayPal's approval page). - The user approves the payment on PayPal.
- PayPal returns them to
…/payments/paypal/return(a route the library itself mounts from yourpublic_base_url). There the library captures the order and then redirects the browser to yoursuccess_url(orcancel_urlif it wasn't captured).
TIP
You don't implement the return route: the library exposes it and PayPal uses it as the return_url. You only provide success_url / cancel_url in the charge body and public_base_url in the config.
Notification and confirmation
Besides the capture on the browser return, PayPal sends a webhook to POST /webhooks/paypal. The library:
- Verifies the signature by calling the PayPal API with your
webhook_id. Invalid →400. - Reads the
event_type:PAYMENT.CAPTURE.COMPLETED→ captured → emitspayment.captured.PAYMENT.CAPTURE.DENIED→ failed → emitspayment.failed.
The gateway_ref that matches the notification to the Payment is the PayPal order id, generated by the library when creating the charge. Idempotency by (gateway, event_id) keeps the browser return and the webhook from being processed twice.