payfyio
Providers

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

  1. Open the PayPal Developer Dashboard and sign in with a PayPal account.
  2. Go to Apps & Credentials. Toggle Sandbox (for testing) or Live (for production) at the top.
  3. Click Create App, name it, and choose the Merchant type.
  4. Copy the Client ID and reveal the Secret. These map to:
    • apiKeyClient ID
    • secretKeySecret

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 order

The 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

  1. Set mode: 'sandbox' and use your Sandbox app's Client ID / Secret.
  2. Call initThreeDSPayment and open the approval URL from init.threeDSHtmlContent (the order id is init.paymentId).
  3. 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.
  4. 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:paypal creates the order and prints the approval URL, and pnpm scenario:paypal-capture <orderId> captures it. See the playground/ 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);

On this page