Last updated: June 30, 2026
Accept PayPal payments on your website with the minimum amount of code required. You might use this ultra light integration as a proof of concept, basic e-commerce checkout, or simple donation form.
This integration creates an order and captures a payment. It uses PayPal's JavaScript SDK on the client side and server SDK on the server side.
The JavaScript SDK renders the PayPal button and handles the UI. The server SDK creates and captures orders with your app credentials.
The code examples on this page use USD and US-based scenarios. PayPal supports multiple currencies and countries. See currency codes and country codes for the full list, and payment methods for country-specific options.
This integration works everywhere PayPal is available.
Choose your preferred language and set up the project structure.
# Create project structure
mkdir paypal-minimal && cd paypal-minimal
npm init -y
npm install express @paypal/checkout-server-sdk dotenv
mkdir public
# Create .env file with development defaults
cat > .env << 'EOF'
# Development/Sandbox Configuration (Default)
# ==========================================
PAYPAL_CLIENT_ID=your_sandbox_client_id
PAYPAL_CLIENT_SECRET=your_sandbox_secret
NODE_ENV=development
PORT=3000
# Negative Testing (Sandbox Only)
# --------------------------------
# Set to true to enable error simulation in sandbox
ENABLE_NEGATIVE_TESTING=false
NEGATIVE_TEST_TYPE=INSUFFICIENT_FUNDS
# Options: INSUFFICIENT_FUNDS, INSTRUMENT_DECLINED, TRANSACTION_REFUSED,
# INTERNAL_SERVER_ERROR, DUPLICATE_INVOICE_ID
# Production Configuration (Uncomment to use)
# ==========================================
# PAYPAL_CLIENT_ID=your_production_client_id
# PAYPAL_CLIENT_SECRET=your_production_secret
# NODE_ENV=production
# PORT=3000
EOF
# Create empty files
touch server.js
touch public/index.html
echo "✅ Project structure created! Now copy the code below into the files."Choose your preferred language and copy the code into your server file:
# Create order
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-d '{
"intent": "CAPTURE",
"purchase_units": [{
"amount": {
"currency_code": "USD",
"value": "10.00"
}
}]
}'
# Capture order
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders/ORDER_ID/capture \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ACCESS_TOKEN"Open the public/index.html file in a code editor and copy and paste the following code. Replace YOUR_CLIENT_ID with the client ID created in the prerequisites.
If your app uses PayPal buttons across multiple pages or a modular JavaScript architecture, see Multipage and modular app issues.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>PayPal Checkout</title>
</head>
<body>
<div id="paypal-button-container"></div>
<div id="result-message"></div>
<script>
// Pass the createOrder, onApprove, onCancel, and onError callbacks to the SDK
async function createOrder(data) {
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// We don't recommend passing order amounts from the client
// Your API should exchange a SKU for amount values
body: JSON.stringify({ sku: "BDUR24586" }),
});
const { id } = await response.json();
return { orderId: id };
}
async function onApprove(data) {
const { orderId } = data;
const response = await fetch(`/api/orders/${orderId}/capture`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const orderData = await response.json();
return orderData;
}
function onCancel(data) {
document.getElementById("result-message").innerHTML =
`<p style="color: orange;">Payment cancelled</p>`;
}
function onError(err) {
console.error("PayPal Error:", err);
let errorMessage = "An error occurred. Please try again.";
// Handle specific error codes
if (err.message && err.message.includes("INSUFFICIENT_FUNDS")) {
errorMessage = "Payment declined due to insufficient funds.";
} else if (err.message && err.message.includes("INSTRUMENT_DECLINED")) {
errorMessage =
"Your payment method was declined. Please try another.";
} else if (err.message && err.message.includes("TRANSACTION_REFUSED")) {
errorMessage = "Transaction was refused. Please contact support.";
} else if (err.message && err.message.includes("DUPLICATE_INVOICE")) {
errorMessage =
"This order has already been processed. Please refresh and try again.";
}
document.getElementById("result-message").innerHTML =
`<p style="color: red;">${errorMessage}</p>`;
}
// After the browser is done downloading the SDK, it will invoke onLoad()
// and begin the SDK setup
async function onLoad() {
try {
const sdkInstance = await window.paypal.createInstance({
clientId: "YOUR_CLIENT_ID", // clientId is safe to put in plaintext on the client
components: ["paypal-payments"],
});
const eligibility = await sdkInstance.findEligibleMethods();
const isPayPalEligible = eligibility.isEligible("paypal");
if (isPayPalEligible) {
const paypalButton = document.createElement("paypal-button");
document
.querySelector("#paypal-button-container")
.append(paypalButton);
const paypalCheckoutSession =
await sdkInstance.createPayPalOneTimePaymentSession({
onApprove,
onCancel,
onError,
});
paypalButton.addEventListener("click", async () => {
try {
// Get the promise reference by invoking createOrder()
// Do not await this async function since it can cause transient activation issues
const createOrderPromise = createOrder();
await paypalCheckoutSession.start(
{ presentationMode: "auto" },
createOrderPromise,
);
} catch (error) {
console.error(error);
}
});
}
} catch (error) {
console.error(error);
}
}
</script>
<script
async
src="https://www.sandbox.paypal.com/web-sdk/v6/core"
onload="onLoad()"
></script>
</body>
</html>Open the .env file in a code editor and replace your_sandbox_client_id and your_sandbox_secret with the client ID and secret you created for your app in the prerequisites.
Start the server:
node server.jsOpen the following URL in your browser. You should see a PayPal button.
http://localhost:3000Use the following best practices to ensure secure, reliable, and maintainable integrations when building your checkout experience.
Before testing, make sure you have sandbox account credentials for both buyer and seller roles. Get sandbox account credentials.
Use the following standard test scenarios to verify your checkout handles key outcomes.
| Test scenario | Setup | Expected result |
|---|---|---|
| Successful payment | Default settings | Green success message and transaction ID shown. |
| Cancelled payment | Default settings | Orange cancel message shown. |
| Server error | Stop server | Red error message shown. |
Negative testing works only in sandbox mode. It is automatically disabled when NODE_ENV=production.
For negative testing:
.env file, set ENABLE_NEGATIVE_TESTING=true and set NEGATIVE_TEST_TYPE to one of the error codes in the table..env file: node server.js.| Test scenario | Error code | Expected result |
|---|---|---|
| Insufficient funds | INSUFFICIENT_FUNDS | Error: buyer cannot pay at capture. |
| Instrument declined | INSTRUMENT_DECLINED | Error: payment method was declined at capture. |
| Internal server error | INTERNAL_SERVER_ERROR | Error: 500 error occurred at create or capture. |
| Transaction refused | TRANSACTION_REFUSED | Error: transaction refused at capture. |
| Duplicate invoice ID | DUPLICATE_INVOICE_ID | Error: invoice ID must be unique at create. |
.env file:
These values are suggested monitoring thresholds for your integration, not performance guarantees from PayPal.
| Metric | Target | Action if below target |
|---|---|---|
| Payment success rate | 95% | Check API errors in logs |
| Cancel rate | <20% | Review checkout UX and pricing display |
| Error rate | <2% | Check error logs and API responses |
| Order creation response time | <2 seconds | Optimize server performance and check PayPal status |
| Capture response time | <3 seconds | Review server performance |
The following use cases are common next steps after completing this quick start.