All posts
paypalpayments

PayPal Integration: A Practical Guide for Full-Stack Developers

A practical guide to integrating PayPal — Orders API, capturing payments, webhooks, and avoiding common verification gaps.

SR

Suhail Roushan

August 6, 2026

·
4 min read
·
0 views

PayPal remains one of the most recognized payment brands globally, and for a meaningful share of users — especially outside the US — it's a trusted, preferred checkout option worth supporting alongside (or instead of) card-only payment flows.

PayPal's modern integration path centers on the Orders API: create an order server-side, let the user approve it via PayPal's checkout button or redirect flow, then capture the payment server-side. The pattern echoes Stripe's and Razorpay's create-then-confirm flows, with PayPal-specific details in how orders are structured and captured.

Why PayPal Integration Matters (and When to Skip It)

For certain markets and demographics, PayPal is a strongly preferred checkout method — offering it alongside card payments can measurably improve conversion for those users specifically. It also handles buyer/seller protection and dispute resolution as part of its platform, which some merchants and customers specifically value.

Skip PayPal if your user base and market don't show meaningful PayPal usage, and you'd rather keep a single payment provider (Stripe, for instance) to reduce integration and reconciliation complexity — supporting multiple payment providers adds real ongoing maintenance cost.

Getting Started with PayPal

Creating an order server-side:

async function createOrder(amount: string) {
  const res = await fetch("https://api-m.paypal.com/v2/checkout/orders", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${await getAccessToken()}`,
    },
    body: JSON.stringify({
      intent: "CAPTURE",
      purchase_units: [{ amount: { currency_code: "USD", value: amount } }],
    }),
  });
  return res.json();
}

Capturing the order after user approval:

async function captureOrder(orderId: string) {
  const res = await fetch(`https://api-m.paypal.com/v2/checkout/orders/${orderId}/capture`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${await getAccessToken()}`,
    },
  });
  return res.json();
}

Core PayPal Concepts Every Developer Should Know

Capture is the step that actually moves money, not order creation. Creating an order just sets up the intent; the payment isn't finalized until your server calls capture after the user approves it in the PayPal UI — fulfillment should be gated on a successful capture response, not just order creation.

Webhooks provide the reliable server-to-server confirmation, the same principle as Stripe and Razorpay: a client-side approval callback can be interrupted or spoofed, so webhook-driven fulfillment (listening for PAYMENT.CAPTURE.COMPLETED) is the more robust pattern for anything beyond simple UX feedback.

app.post("/webhooks/paypal", async (req, res) => {
  const isValid = await verifyPaypalWebhookSignature(req);
  if (!isValid) return res.status(400).send("Invalid signature");

  if (req.body.event_type === "PAYMENT.CAPTURE.COMPLETED") {
    await fulfillOrder(req.body.resource);
  }
  res.sendStatus(200);
});

OAuth access tokens for the API are short-lived and need refreshing, unlike an API-key-based provider — your server needs to fetch and cache a token, refreshing it before it expires, as part of any PayPal API call flow.

Sandbox and live environments use entirely separate API base URLs and credentials, worth being deliberate about in configuration to avoid accidentally testing against (or worse, shipping test credentials pointed at) the wrong environment.

Common PayPal Mistakes and How to Fix Them

Mistake 1: fulfilling orders based on the client-side approval flow alone, without confirming capture succeeded server-side. This is the same exploitable gap as trusting a client-side callback in any other payment integration. Fix: gate fulfillment on a successful server-side capture response or verified webhook event.

Mistake 2: not verifying webhook signatures, trusting webhook payloads without validation. Fix: use PayPal's webhook signature verification API before acting on any webhook event.

Mistake 3: mixing sandbox and live credentials/URLs, a surprisingly common source of "why isn't this working" during integration testing. Fix: keep sandbox and live configuration clearly separated with environment-specific credentials and explicit base URLs.

When Should You Use PayPal Instead of Stripe?

Use PayPal alongside or instead of Stripe when your specific market or user base shows meaningful preference for it, or when PayPal's buyer/seller protection model specifically matters for your business type. Use Stripe as the primary or sole provider when card payments dominate your user base and you want a single, deeply integrated payments platform rather than reconciling multiple providers.

PayPal Integration in Production

Test extensively in the sandbox environment, including failure and refund/dispute paths, before going live. Also implement webhook-based fulfillment as the reliable confirmation path, with the client-side approval flow treated as UX only — the same discipline every payment integration in this space needs.

Before launch, confirm capture confirmation (not just order creation) gates fulfillment, and that webhook signatures are verified — those are the details most likely to create an exploitable gap if rushed.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch