PayPal REST API Indonesia 2026 — Integration Developer Next.js

·ChatBot Cell·12 menit baca
PayPal
PayPal REST API Indonesia 2026 — Integration Developer Next.js
Daftar Isi

PayPal REST API — Backbone Integration buat Developer Indonesia

Lo developer Indonesia. Mau integrate PayPal checkout ke web app lo. Pakai WordPress plugin? Terlalu kaku. Pakai third-party gateway? Mahal. Solusinya: direct REST API integration.

PayPal REST API powerful, well-documented, dan punya semua feature yang lo butuh: one-time payment, subscription, refund, payout, dispute, invoicing.

Panduan ini bahas cara integrate PayPal REST API di Next.js 16 dengan TypeScript.

Singkatnya: PayPal REST API = OAuth2 + endpoints + webhook. Next.js 16 API route handle server-side, client SDK buat button. Butuh bantu integrate? Chat ChatBot Cell.

1. Konsep Dasar PayPal REST API

Arsitektur

[Browser Client]
    ↓ (PayPal Smart Button SDK)
[PayPal JS SDK]
    ↓ (HTTPS)
[PayPal API Gateway]
    ↑ (server-to-server, OAuth2 token)
[Your Next.js API Route]
    ↑
[Your DB]

Komponen Utama

  1. Client SDK (@paypal/react-paypal-js): render button di browser
  2. Server API Route (/app/api/paypal/...): OAuth2, create order, capture, webhook
  3. Webhook Handler (/app/api/paypal/webhook/route.ts): receive event async
  4. Database: store order, payment, subscription state

Environment

Env Base URL Pakai Kapan
Sandbox api-m.sandbox.paypal.com Dev + test
Live api-m.paypal.com Production

2. Setup Project

Install Dependency

npm install @paypal/react-paypal-js
npm install -D typescript @types/node

Environment Variables

# .env.local
PAYPAL_ENV=sandbox
PAYPAL_CLIENT_ID=ATmxZxxx
PAYPAL_CLIENT_SECRET=EGj3xxx
PAYPAL_WEBHOOK_ID=WH-XXXXX
PAYPAL_API_BASE=https://api-m.sandbox.paypal.com
NEXT_PUBLIC_PAYPAL_CLIENT_ID=ATmxZxxx

TypeScript Types

// types/paypal.ts
export interface PayPalOrder {
  id: string;
  status: "CREATED" | "SAVED" | "APPROVED" | "VOIDED" | "COMPLETED";
  intent: "CAPTURE" | "AUTHORIZE";
  purchase_units: PurchaseUnit[];
  links: PayPalLink[];
}

export interface PurchaseUnit {
  amount: { currency_code: string; value: string };
  description?: string;
  reference_id?: string;
}

export interface PayPalLink {
  href: string;
  rel: string;
  method: string;
}

3. OAuth2 Token Management

Server-Side Token Cache

// lib/paypal/auth.ts
const tokenCache = { token: "", expiresAt: 0 };

export async function getAccessToken(): Promise<string> {
  if (tokenCache.token && Date.now() < tokenCache.expiresAt - 60_000) {
    return tokenCache.token;
  }
  
  const auth = Buffer.from(
    `${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
  ).toString("base64");
  
  const res = await fetch(
    `${process.env.PAYPAL_API_BASE}/v1/oauth2/token`,
    {
      method: "POST",
      headers: {
        Authorization: `Basic ${auth}`,
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: "grant_type=client_credentials",
      cache: "no-store",
    }
  );
  
  if (!res.ok) {
    throw new Error(`PayPal auth failed: ${res.status}`);
  }
  
  const data = await res.json();
  tokenCache.token = data.access_token;
  tokenCache.expiresAt = Date.now() + data.expires_in * 1000;
  
  return data.access_token;
}

Critical: Pakai in-memory cache untuk single-instance. Kalau multi-instance (Vercel, Kubernetes), pakai Redis atau Upstash untuk shared cache.

4. Create Order Endpoint

API Route

// app/api/paypal/create-order/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAccessToken } from "@/lib/paypal/auth";

export async function POST(req: NextRequest) {
  const { items, currency = "USD" } = await req.json();
  
  const total = items.reduce(
    (sum: number, item: { amount: number; quantity: number }) =>
      sum + item.amount * item.quantity,
    0
  );
  
  const token = await getAccessToken();
  
  const res = await fetch(
    `${process.env.PAYPAL_API_BASE}/v2/checkout/orders`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
        "PayPal-Request-Id": crypto.randomUUID(),
      },
      body: JSON.stringify({
        intent: "CAPTURE",
        purchase_units: [
          {
            amount: {
              currency_code: currency,
              value: total.toFixed(2),
            },
            description: `Order ${Date.now()}`,
            items: items.map((item: { name: string; amount: number; quantity: number }) => ({
              name: item.name,
              unit_amount: {
                currency_code: currency,
                value: item.amount.toFixed(2),
              },
              quantity: String(item.quantity),
            })),
          },
        ],
      }),
    }
  );
  
  const order = await res.json();
  
  if (!res.ok) {
    return NextResponse.json({ error: order }, { status: 500 });
  }
  
  return NextResponse.json({ id: order.id });
}

5. Capture Order Endpoint

// app/api/paypal/capture-order/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAccessToken } from "@/lib/paypal/auth";
import { updateOrderStatus } from "@/lib/db";

export async function POST(req: NextRequest) {
  const { orderID } = await req.json();
  
  const token = await getAccessToken();
  
  const res = await fetch(
    `${process.env.PAYPAL_API_BASE}/v2/checkout/orders/${orderID}/capture`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
    }
  );
  
  const captureData = await res.json();
  
  if (!res.ok) {
    return NextResponse.json({ error: captureData }, { status: 500 });
  }
  
  // Update DB (defensive: webhook juga akan trigger update)
  await updateOrderStatus(orderID, "PAID", captureData);
  
  return NextResponse.json({ status: "success", capture: captureData });
}

6. Client Component (Smart Button)

// app/components/PayPalCheckout.tsx
"use client";

import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";

export default function PayPalCheckout({ items, total }) {
  return (
    <PayPalScriptProvider
      options={{
        clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID!,
        currency: "USD",
        intent: "capture",
      }}
    >
      <PayPalButtons
        style={{ layout: "vertical", color: "gold", shape: "rect" }}
        createOrder={async () => {
          const res = await fetch("/api/paypal/create-order", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ items }),
          });
          const { id } = await res.json();
          return id;
        }}
        onApprove={async (data) => {
          const res = await fetch("/api/paypal/capture-order", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ orderID: data.orderID }),
          });
          const result = await res.json();
          if (result.status === "success") {
            window.location.href = "/order/success";
          } else {
            alert("Payment failed. Coba lagi.");
          }
        }}
        onError={(err) => {
          console.error("PayPal error:", err);
          alert("Terjadi kesalahan. Hubungi support.");
        }}
      />
    </PayPalScriptProvider>
  );
}

7. Webhook Handler

Rekomendasi · Sponsored

Promo seru yang cocok buat kamu

Penawaran pilihan dari mitra kami — klik buat lihat detail.

Lihat

Mengandung link afiliasi. Baca disclaimer.

// app/api/paypal/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAccessToken } from "@/lib/paypal/auth";
import { handleWebhookEvent } from "@/lib/paypal/webhook-handler";

export async function POST(req: NextRequest) {
  const body = await req.text();
  const headers = Object.fromEntries(req.headers.entries());
  
  const verified = await verifyWebhookSignature(headers, JSON.parse(body));
  if (!verified) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }
  
  const event = JSON.parse(body);
  
  // Idempotent handling
  const alreadyProcessed = await checkProcessedEvent(event.id);
  if (alreadyProcessed) {
    return NextResponse.json({ status: "duplicate" });
  }
  
  try {
    await handleWebhookEvent(event);
    await markEventProcessed(event.id);
  } catch (err) {
    console.error("Webhook handler error:", err);
    return NextResponse.json({ error: "Handler failed" }, { status: 500 });
  }
  
  return NextResponse.json({ status: "success" });
}

async function verifyWebhookSignature(headers: Record<string, string>, body: any) {
  const token = await getAccessToken();
  
  const res = await fetch(
    `${process.env.PAYPAL_API_BASE}/v1/notifications/verify-webhook-signature`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        auth_algo: headers["paypal-auth-algo"],
        cert_url: headers["paypal-cert-url"],
        transmission_id: headers["paypal-transmission-id"],
        transmission_sig: headers["paypal-transmission-sig"],
        transmission_time: headers["paypal-transmission-time"],
        webhook_id: process.env.PAYPAL_WEBHOOK_ID,
        webhook_event: body,
      }),
    }
  );
  
  const data = await res.json();
  return data.verification_status === "SUCCESS";
}

8. Webhook Event Handler

// lib/paypal/webhook-handler.ts
import { updateOrderStatus, activateSubscription, refundProcessed } from "@/lib/db";

export async function handleWebhookEvent(event: any) {
  switch (event.event_type) {
    case "PAYMENT.CAPTURE.COMPLETED":
      await updateOrderStatus(event.resource.id, "PAID", event.resource);
      await sendConfirmationEmail(event.resource.custom_id);
      break;
    
    case "PAYMENT.CAPTURE.DENIED":
      await updateOrderStatus(event.resource.id, "DENIED", event.resource);
      break;
    
    case "PAYMENT.CAPTURE.REFUNDED":
      await refundProcessed(event.resource.id, event.resource);
      break;
    
    case "BILLING.SUBSCRIPTION.ACTIVATED":
      await activateSubscription(event.resource.id, event.resource);
      break;
    
    case "BILLING.SUBSCRIPTION.CANCELLED":
      await cancelSubscription(event.resource.id);
      break;
    
    case "CUSTOMER.DISPUTE.CREATED":
      await createDisputeTicket(event.resource);
      await notifyAdminDispute(event.resource);
      break;
    
    default:
      console.log(`Unhandled event: ${event.event_type}`);
  }
}

9. Subscription Integration

Create Subscription Plan

// app/api/paypal/create-plan/route.ts
export async function POST() {
  const token = await getAccessToken();
  
  const res = await fetch(
    `${process.env.PAYPAL_API_BASE}/v1/billing/plans`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        product_id: "PROD-XXXX",
        name: "Pro Monthly",
        description: "Pro tier subscription",
        billing_cycles: [
          {
            frequency: { interval_unit: "MONTH", interval_count: 1 },
            tenure_type: "REGULAR",
            sequence: 1,
            pricing_scheme: {
              fixed_price: { value: "19.00", currency_code: "USD" },
            },
          },
        ],
        payment_preferences: {
          auto_bill_outstanding: true,
          setup_fee: { value: "0", currency_code: "USD" },
          setup_fee_failure_action: "CONTINUE",
        },
      }),
    }
  );
  
  return NextResponse.json(await res.json());
}

10. Refund Endpoint

// app/api/paypal/refund/route.ts
export async function POST(req: NextRequest) {
  const { captureId, amount } = await req.json();
  
  const token = await getAccessToken();
  
  const body = amount
    ? { amount: { value: amount, currency_code: "USD" } }
    : {};
  
  const res = await fetch(
    `${process.env.PAYPAL_API_BASE}/v2/payments/captures/${captureId}/refund`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    }
  );
  
  const refund = await res.json();
  return NextResponse.json(refund);
}

11. Studi Kasus — Indonesia SaaS Build PayPal Integration

Profil: Joko, founder Indonesia SaaS project management tool. Target: US + SG SMB. Pricing $29/month + $290/year.

Tech Stack

  • Next.js 16 (App Router)
  • PostgreSQL (Neon)
  • Prisma ORM
  • Vercel hosting
  • PayPal REST API + Webhook

Implementation Time

  • Day 1-2: OAuth2 + sandbox setup
  • Day 3-4: Create order + capture (one-time)
  • Day 5-7: Subscription + lifecycle webhook
  • Day 8: Refund + cancellation
  • Day 9: Webhook verify + idempotent
  • Day 10: Testing (Cypress E2E)
  • Day 11: Production go-live
  • Day 12: Monitor + bug fix

Code Metrics

  • ~1.500 LOC buat PayPal integration
  • 8 API routes (create-order, capture, refund, webhook, create-plan, cancel-sub, billing-history, customer-portal)
  • 3 React components (PayPalCheckout, PayPalSubscription, CustomerPortal)
  • 50+ unit tests
  • 12 E2E test scenarios

Result Month 1

  • 142 customer subscribed
  • 4% visit → paid conversion
  • 0 production bug
  • 1 dispute (won via webhook auto-respond)
  • $3.800 MRR

Lesson: REST API integration = full control + custom UX. Worth invest waktu 10-12 hari.

12. Error Handling Best Practice

Retry Logic

async function callPayPalWithRetry(fn: () => Promise<Response>, maxRetry = 3) {
  for (let i = 0; i < maxRetry; i++) {
    try {
      const res = await fn();
      if (res.status === 429 || res.status >= 500) {
        await new Promise((r) => setTimeout(r, 2 ** i * 1000));
        continue;
      }
      return res;
    } catch (err) {
      if (i === maxRetry - 1) throw err;
      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
  }
  throw new Error("PayPal API max retry exceeded");
}

Idempotency Key

const requestId = crypto.randomUUID();

const res = await fetch(url, {
  method: "POST",
  headers: {
    "PayPal-Request-Id": requestId,
  },
});

PayPal dedupe request dengan Request-Id sama dalam 24 jam.

13. Common Mistake Developer

Mistake 1: Client-Side Capture

Mistake: call capture API dari browser (with secret). Fix: capture SELALU server-side. Client cuma trigger button.

Mistake 2: Nggak Verify Webhook Signature

Mistake: trust webhook masuk. Fix: always verify. Forged webhook = hacker refund themselves.

Mistake 3: Trust Client Amount

Mistake: client set amount di create-order. Fix: server calculate dari DB, NEVER trust client input.

Mistake 4: Skip Idempotency

Mistake: retry request tanpa idempotency key. Fix: always include PayPal-Request-Id.

Mistake 5: Log Secret

Mistake: console.log(request body including secret). Fix: redact sensitive fields. Pakai logger structured.

Mistake 6: Nggak Handle Network Failure

Mistake: assume PayPal always respond. Fix: timeout 30s + retry logic + fallback queue.

Mistake 7: Webhook URL HTTP

Mistake: webhook URL HTTP (PayPal reject). Fix: HTTPS wajib. Pakai ngrok untuk dev.

14. Testing PayPal API

Unit Test

// __tests__/paypal.test.ts
import { describe, it, expect, vi } from "vitest";

describe("PayPal create order", () => {
  it("should create order with correct amount", async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ id: "ORDER-123" }),
    });
    global.fetch = mockFetch as any;
    
    const result = await createOrder([
      { name: "Item 1", amount: 10, quantity: 2 },
    ]);
    
    expect(result.id).toBe("ORDER-123");
    expect(mockFetch).toHaveBeenCalledWith(
      expect.stringContaining("/v2/checkout/orders"),
      expect.objectContaining({ method: "POST" })
    );
  });
});

E2E Test

// cypress/e2e/paypal-subscription.cy.ts
describe("PayPal Subscription", () => {
  it("completes subscription signup", () => {
    cy.visit("/pricing");
    cy.get("[data-cy=subscribe-pro]").click();
    
    cy.origin("https://www.sandbox.paypal.com", () => {
      cy.get("#email").type("test-buyer@example.com");
      cy.get("#password").type("Test1234!");
      cy.get("#btnLogin").click();
      cy.get("#confirmButtonTop", { timeout: 15000 }).click();
    });
    
    cy.url().should("include", "/dashboard");
    cy.contains("Pro Active").should("be.visible");
  });
});

15. Production Checklist

Pre-Launch

  • Sandbox test passing
  • Live credentials set
  • HTTPS enforced
  • Webhook signature verify ON
  • Idempotency key implemented
  • Error monitoring (Sentry)
  • Log webhook raw (90 hari retention)
  • Reconciliation cron job

Security

  • Secret NOT in client bundle
  • CORS restricted to own domain
  • Rate limiting ON (per IP)
  • SQL injection prevention (parameterized)
  • XSS prevention (escape user input)
  • CSRF token for state-changing endpoints

Compliance

  • Privacy policy
  • Terms of service
  • Refund policy
  • Cookie consent (GDPR)
  • PCI DSS SAQ-A scope

16. FAQ PayPal API Indonesia

Q: Bisanya Indonesia developer daftar PayPal API?

A: Bisa. PayPal developer account gratis. API access global.

Q: Apakah perlu LLC US buat PayPal API?

A: Nggak. Personal/Business account Indonesia cukup. LLC needed only buat high volume.

Q: Berapa rate limit PayPal API?

A: 4.000 request/menit per app. Lebih dari itu = 429 error.

Q: Bisanya webhook delay?

A: Kadang 1-30 detik. Selalu implement reconciliation polling.

Q: Best library PayPal Node.js?

A: @paypal/react-paypal-js (client) + raw fetch (server). Hindari paypal-rest-sdk (deprecated).

17. Mitos vs Fakta PayPal API

Mitos 1: "PayPal API Ribet"

Fakta: 4 endpoint dasar: OAuth, create-order, capture, webhook. Sisanya fitur tambahan.

Mitos 2: "SDK Mahal"

Fakta: SDK gratis. Cuma transaction fee (4.4% + $0.30).

Mitos 3: "Webhook Optional"

Fakta: Wajib buat reliable system. Capture return false positive kalau buyer cancel before redirect.

Mitos 4: "Live Test Sama dengan Sandbox"

Fakta: 95% sama. Fraud + 3DS beda di live.

Mitos 5: "Sekali Setup, Selesai Selamanya"

Fakta: PayPal update API version tiap tahun. Maintain SDK update.

18. Verdict — REST API = Full Control Integration

PayPal REST API = jalur professional buat Indonesia developer yang mau full control + custom UX + scale.

Yang paling critical:

  • Server-side capture (NEVER client-side)
  • Webhook verify signature
  • Idempotency key
  • OAuth2 token cache
  • Error handling + retry

Yang perlu di-avoid:

  • Client-side capture
  • Trust client amount
  • Skip webhook verify
  • Hardcode secret
  • HTTP webhook URL

Yang always do:

  • Log raw webhook
  • Reconciliation job
  • E2E test Cypress
  • Monitor conversion rate
  • Update SDK rutin

ChatBot Cell siap bantu setup PayPal REST API integration + Next.js + webhook handler + production deploy. Plus AI Chatbot buat auto-handle webhook + alert anomaly + reconcile drift. Konsultasi gratis.

👉 Mau integrate PayPal REST API? Chat ChatBot Cell