Skip to main content

Overview

The PayPal API provides two endpoints for processing payments:
  1. Create Order - Initialize a PayPal order
  2. Capture Order - Capture payment after customer approval
PayPal payments require a two-step process: first create an order, then capture it after the customer approves the payment through PayPal’s interface.

POST /api/paypal/create-order

Create a new PayPal order for processing subscription payments. This endpoint validates the plan and pricing, then returns a PayPal order ID for customer approval.

Rate Limiting

  • Limit: 10 requests per IP
  • Window: 15 minutes
  • Purpose: Prevent order creation spam

Security Features

  • Server-side price validation (prevents tampering)
  • Plan and duration validation
  • Input sanitization
  • PayPal OAuth authentication

Request Body

planId
string
required
Plan identifier (max 50 characters)Must be one of: plan-1 (Basic), plan-2 (Standard), plan-3 (Premium)Example: "plan-2"
months
number
required
Subscription duration in months (1-12 range)Must be one of: 1, 2, 3, 6, or 12Example: 6
amount
number
required
Payment amount in USD (1-10000 range)Must exactly match the plan’s price for the selected duration. The server validates this to prevent price manipulation (tolerance: 0.01).Example: 70

Response

id
string
PayPal order IDUse this ID to:
  1. Redirect customer to PayPal for approval
  2. Capture the payment after approval
Example: "7XX12345XX123456X"

Example Request

curl -X POST https://your-domain.com/api/paypal/create-order \
  -H "Content-Type: application/json" \
  -d '{
    "planId": "plan-2",
    "months": 6,
    "amount": 70
  }'

Example Response

200 OK
{
  "id": "7XX12345XX123456X"
}

Error Responses


POST /api/paypal/capture-order

Capture a PayPal order after the customer has approved the payment. This finalizes the transaction and charges the customer.
Only call this endpoint after the customer has approved the payment through PayPal’s interface. The order must be in APPROVED status.

Rate Limiting

  • No rate limit - Captures are expected to follow creations

Request Body

orderId
string
required
PayPal order ID to captureThis is the ID returned from /api/paypal/create-orderExample: "7XX12345XX123456X"

Response

id
string
Captured order ID (same as input)
status
string
Order status after captureTypically: "COMPLETED"

Example Request

curl -X POST https://your-domain.com/api/paypal/capture-order \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "7XX12345XX123456X"
  }'

Example Response

200 OK
{
  "id": "7XX12345XX123456X",
  "status": "COMPLETED"
}

Error Responses


PayPal Order Structure

The order created by /api/paypal/create-order has the following structure:
{
  "intent": "CAPTURE",
  "purchase_units": [
    {
      "amount": {
        "currency_code": "USD",
        "value": "70.00"
      },
      "description": "Connect World Plan plan-2 6mo"
    }
  ]
}
intent
string
Always set to "CAPTURE" for immediate payment capture
purchase_units
array
Array containing payment details
  • amount.currency_code: Always "USD"
  • amount.value: Formatted price (e.g., "70.00")
  • description: Human-readable description with plan and duration

Authentication

Both endpoints use PayPal OAuth for authentication:
// Authentication flow (from create-order/route.ts:10-27)
async function getPayPalAccessToken(): Promise<string> {
  const credentials = Buffer.from(
    `${process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
  ).toString('base64');

  const { data } = await axios.post(
    `${PAYPAL_BASE}/v1/oauth2/token`,
    'grant_type=client_credentials',
    {
      headers: {
        Authorization: `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  return data.access_token;
}

Implementation Details

Create Order: Implemented in /src/app/api/paypal/create-order/route.ts:29Capture Order: Implemented in /src/app/api/paypal/capture-order/route.ts:25Both endpoints use:
  • HTTP Client: axios
  • Base URL: PAYPAL_BASE_URL (sandbox or production)
  • Authentication: OAuth 2.0 with client credentials
  • Security: Rate limiting (create only), input sanitization, price validation

Complete Payment Flow

  1. Client: Call /api/paypal/create-order with plan details
  2. Server: Validate plan, months, and amount
  3. Server: Authenticate with PayPal OAuth
  4. Server: Create PayPal order
  5. Server: Return order ID
  6. Client: Redirect user to PayPal approval URL
  7. Customer: Approve payment on PayPal
  8. PayPal: Redirect back to your site with order ID
  9. Client: Call /api/paypal/capture-order with order ID
  10. Server: Capture the payment
  11. Server: Return captured order status
  12. Client: Call /api/orders with captured order ID as paymentReceiptId
  13. Server: Create subscription order
  14. Client: Show success confirmation

Example: Full Integration

import { PayPalButtons } from '@paypal/react-paypal-js';

function PayPalCheckout({ planId, months, amount, customerData }) {
  const createOrder = async () => {
    // Step 1: Create PayPal order
    const response = await fetch('/api/paypal/create-order', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ planId, months, amount }),
    });
    const { id } = await response.json();
    return id;
  };

  const onApprove = async (data) => {
    // Step 2: Capture the payment
    const captureResponse = await fetch('/api/paypal/capture-order', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ orderId: data.orderID }),
    });
    const { id, status } = await captureResponse.json();

    if (status === 'COMPLETED') {
      // Step 3: Create subscription order
      await fetch('/api/orders', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...customerData,
          planId,
          months,
          amount,
          paymentMethod: 'paypal',
          paymentReceiptId: id,
        }),
      });

      alert('Subscription created successfully!');
    }
  };

  return (
    <PayPalButtons
      createOrder={createOrder}
      onApprove={onApprove}
      style={{ layout: 'vertical' }}
    />
  );
}

Testing

For testing, use PayPal sandbox environment:
  • Set PAYPAL_BASE_URL to https://api-m.sandbox.paypal.com
  • Use sandbox client ID and secret
  • Create test buyer accounts in PayPal Developer Dashboard
  • Test cards: Use PayPal’s test credit cards

Test Scenarios

ScenarioExpected Result
Valid plan and priceReturns order ID
Invalid plan ID400 error: “Plan inválido.”
Wrong amount400 error: “Monto inválido.”
Invalid duration400 error: “Duración inválida.”
Capture unapproved order500 error with PayPal details
Capture twice500 error: already captured

Environment Variables

Required environment variables:
# PayPal Credentials
NEXT_PUBLIC_PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...

# Base URL (sandbox or production)
PAYPAL_BASE_URL=https://api-m.sandbox.paypal.com

PayPal Response Logging

The create-order endpoint logs PayPal responses:
// From create-order/route.ts:75-88
console.log('[PayPal create-order] payload:', JSON.stringify(orderPayload));
console.log('[PayPal create-order] created:', data.id, 'status:', data.status);
Check server logs for debugging PayPal integration issues.

See Also

Build docs developers (and LLMs) love