PayPal
PayPal Orders v2 integration with the buyer-approval redirect flow.
PayPal is integrated via the Orders v2 REST API. Authentication is OAuth2 client credentials — apiKey is your client id, secretKey is your client secret. PayPal handles SCA on its own approval page; payfyio returns an HTML redirect to that approval URL through threeDSHtmlContent.
Direct card capture (raw PAN) is not exposed by this provider. PayPal's Advanced Card Processing entitlement varies per merchant. Use the buyer-approval flow described below.
No PayPal SDK required. payfyio calls the PayPal REST API directly over HTTPS. You only ever supply a Client ID and Secret — there is no PayPal-specific package to install and no OAuth/token handling to write yourself.
Getting your credentials
- Open the PayPal Developer Dashboard and sign in with a PayPal account.
- Go to Apps & Credentials. Toggle Sandbox (for testing) or Live (for production) at the top.
- Click Create App, name it, and choose the Merchant type.
- Copy the Client ID and reveal the Secret. These map to:
apiKey← Client IDsecretKey← Secret
Sandbox and Live have separate apps and separate credentials. The mode you pass to payfyio (sandbox / production) must match the credentials you used.
Configuration
paypal: {
enabled: true,
config: {
apiKey: process.env.PAYPAL_CLIENT_ID!,
secretKey: process.env.PAYPAL_CLIENT_SECRET!,
},
}baseUrl defaults to https://api-m.sandbox.paypal.com (sandbox mode) or https://api-m.paypal.com (production mode).
Buyer Approval Flow
// 1) Create the order and redirect the buyer to PayPal.
const init = await payment.paypal.initThreeDSPayment({
price: '49.99',
paidPrice: '49.99',
currency: 'USD',
basketId: 'order-1',
callbackUrl: 'https://yoursite.com/paypal/return',
paymentCard: { /* unused — PayPal collects payment method on its page */ } as any,
buyer: { id, name, surname, email, ip, /* … */ },
shippingAddress: { /* … */ },
billingAddress: { /* … */ },
basketItems: [{ id, name, category1, itemType: 'PHYSICAL', price: '49.99' }],
});
// init.threeDSHtmlContent → render to redirect the user to PayPal.
// 2) On return, PayPal appends `?token=<orderId>&PayerID=…` to your callbackUrl.
const final = await payment.paypal.completeThreeDSPayment(req.query);
// final.status === 'success' on a captured orderThe three calls are distinct steps: create → approve → capture. Money only moves on the capture (completeThreeDSPayment). An order that the buyer never approves — or that you never capture — stays CREATED and silently expires; it produces no transaction in either PayPal account. So if you create an order and see nothing in your PayPal history, that is expected until you complete steps 2 and 3.
Testing in sandbox
- Set
mode: 'sandbox'and use your Sandbox app's Client ID / Secret. - Call
initThreeDSPaymentand open the approval URL frominit.threeDSHtmlContent(the order id isinit.paymentId). - On PayPal's approval page, log in with a personal (buyer) sandbox account — not your business account. Find or create one under Developer Dashboard → Testing Tools → Sandbox Accounts (type
Personal); use the⋮menu to view its email and password. Approve the payment. - Capture it by calling
completeThreeDSPayment({ token: orderId }).
After the capture the transaction appears in both the buyer's and your business sandbox account histories at sandbox.paypal.com. A common mistake is approving with the business account (the seller) instead of the personal buyer account — the approval screen needs a buyer to log in.
This repo ships runnable scripts for exactly this flow:
pnpm scenario:paypalcreates the order and prints the approval URL, andpnpm scenario:paypal-capture <orderId>captures it. See theplayground/package.
Refund
refund looks up the captured payment id from the order, then issues a partial-or-full refund:
await payment.paypal.refund({ paymentId: orderId, price: '49.99', currency: 'USD', ip: '…' });Cancel / Get
// Cancel only succeeds for orders that have not yet been captured.
await payment.paypal.cancel({ paymentId: orderId, ip: '…' });
await payment.paypal.getPayment(orderId);