On this page
No Headings
Last updated: July 10, 2026
This version of the JavaScript SDK integration guide for direct merchants is a legacy integration. Use version 2 for new integrations.
Expanded Checkout lets you offer the PayPal payment button and custom credit and debit card fields. This guide covers integrating the PayPal button, credit, and debit card payments. You can find more ways to extend your integration in our customization doc.
Once you integrate Expanded Checkout, you can also offer options like Pay Later, Venmo, and alternative payment methods with some additional configuration.
This integration guide follows the code in this GitHub sample.

Note: Discover Checkout's advanced features and unlock greater control over your checkout process by directly integrating with PayPal REST APIs. See Get started with PayPal REST API for more information about setting up your own custom integration.
There are 2 versions of card payment integrations for direct merchants using the JavaScript SDK:
CardFields component to accept and save cards without handling card information. PayPal handles all security and compliance issues associated with processing cards. The CardFields component is the recommended integration method and receives ongoing enhancements and improvements.HostedFields component. This integration is no longer under active development and won’t receive further updates.Other elements of the JavaScript SDK integration for direct merchants remain the same.
GitHub Codespaces are cloud-based developer containers that are ready right away. Use GitHub Codespaces to code and test your PayPal integration.
Watch our video tutorial for this integration:
You can use Postman to explore and test PayPal APIs.
1Check your account setup for advanced card payments
This integration requires a sandbox business account with the Advanced Credit and Debit Card Payments capability. Your account should automatically have this capability.
To confirm that Advanced Credit and Debit Card Payments are enabled for you, check your sandbox business account as follows:
Note: If you created a sandbox business account through sandbox.paypal.com, and the Advanced Credit and Debit Card Payments status for the account is disabled, complete the sandbox onboarding steps.
2 Check 3D Secure requirements
Add 3D Secure to reduce the chance of fraud and improve the payment experience by authenticating a card holder through their card issuer.
Visit our 3D Secure page to see if 3D Secure is required in your region and learn more about implementing 3D Secure in your app.
3 Set up npm
One of the requirements to run this sample application is to have npm installed. For more info, visit npm's documentation.
npm install dotenv ejs express node-fetchSelect any of the links below to see more detailed installation instructions for each library:
| Third-party libraries | Description |
|---|---|
| Dotenv | This zero-dependency module separates your configuration and code by loading environment variables from a .env file into process.env. |
| ejs | These templates help you deliver HTML markup using plain JavaScript. |
| express | This lean Node.js web application framework supports web and mobile applications. |
| node-fetch | This function helps you make API requests, similar to window.fetch. |
package.json file has a list of the packages and version numbers needed for your app. You can share your package.json file with other developers so they can use the same settings as you.The following snippet is an example of a package.json file for a PayPal integration. Compare this sample to the package.json in your project:
{
"name": "@paypalcorp/advanced-integration",
"version": "1.0.0",
"description": "",
"main": "YOUR-SERVER-NAME.js",
"type": "module",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "node YOUR-SERVER-NAME.js"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"dotenv": "^16.0.0",
"ejs": "^3.1.6",
"express": "^4.17.3",
"node-fetch": "^3.2.1"
}
}main by replacing the default YOUR-SERVER-NAME.js on lines 5 and 9.scripts.test and scripts.start to customize how your app starts up.If you're having trouble with your app, try reinstalling your local library and package files using npm install.
If you're getting the node error below, you need to include "type": "module" in your package.json file. This line isn't automatically added when package.json is created:
Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension (Use node --trace-warnings ... to show where the warning was created)
See line 6 of the sample package.json file for an example.
.env file is a line-delimited text file that sets your local working environment variables. Use this .env file to securely pass the client ID and app secret for your app.This code shows an example .env file. Rename .env.example to .env and replace the PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET with values from your app:
Note: View your PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET in your PayPal Developer Dashboard under "Apps & Credentials".
PAYPAL_CLIENT_ID="YOUR_CLIENT_ID_GOES_HERE"
PAYPAL_CLIENT_SECRET="YOUR_SECRET_GOES_HERE"This section explains how to set up your back end to integrate Expanded Checkout payments.
Back-end process
Back-end code
The following example uses 2 files, /server/server.js and /server/views/checkout.ejs, to show how to set up the back end to integrate with Expanded Checkout.
/server/server.js
The /server/server.js file provides methods and functions for making the API calls and handling errors, and creates the endpoints that your app uses to:
You'll need to save the server.js file in a folder named /server.
/server/views/checkout.ejs
The /server/views/checkout.ejs file is an Embedded JavaScript (EJS) file that:
You'll need to save the checkout.ejs file in a folder named /server/views.
import express from "express";
import fetch from "node-fetch";
import "dotenv/config";
const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env;
const base = "https://api-m.sandbox.paypal.com";
const app = express();
app.set("view engine", "ejs");
app.set("views", "./server/views");
app.use(express.static("client"));
// parse post params sent in body in json format
app.use(express.json());
/**
* Generate an OAuth 2.0 access token for authenticating with PayPal REST APIs.
* See /api/rest/authentication
*/
const generateAccessToken = async () => {
try {
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
throw new Error("MISSING_API_CREDENTIALS");
}
const auth = Buffer.from(
PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET,
).toString("base64");
const response = await fetch(`${base}/v1/oauth2/token`, {
method: "POST",
body: "grant_type=client_credentials",
headers: {
Authorization: `Basic ${auth}`,
},
});
const data = await response.json();
return data.access_token;
} catch (error) {
console.error("Failed to generate Access Token:", error);
}
};
/**
* Generate a client token for rendering the hosted card fields.
* See https://developer.paypal.com/platforms/checkout/advanced/sdk/v1/#link-integratebackend
*/
const generateClientToken = async () => {
const accessToken = await generateAccessToken();
const url = `${base}/v1/identity/generate-token`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Language": "en_US",
"Content-Type": "application/json",
},
});
return handleResponse(response);
};
/**
* Create an order to start the transaction.
* See /api/orders/v2/orders-create
*/
const createOrder = async (cart) => {
// use the cart information passed from the front-end to calculate the purchase unit details
console.log(
"shopping cart information passed from the frontend createOrder() callback:",
cart,
);
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders`;
const payload = {
intent: "CAPTURE",
purchase_units: [
{
amount: {
currency_code: "USD",
value: "100.00",
},
},
],
};
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
method: "POST",
body: JSON.stringify(payload),
});
return handleResponse(response);
};
/**
* Capture payment for the created order to complete the transaction.
* See /api/orders/v2/orders-capture
*/
const captureOrder = async (orderID) => {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders/${orderID}/capture`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
});
return handleResponse(response);
};
async function handleResponse(response) {
try {
const jsonResponse = await response.json();
return {
jsonResponse,
httpStatusCode: response.status,
};
} catch (err) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
}
// render checkout page with client id & unique client token
app.get("/", async (req, res) => {
try {
const { jsonResponse } = await generateClientToken();
res.render("checkout", {
clientId: PAYPAL_CLIENT_ID,
clientToken: jsonResponse.client_token,
});
} catch (err) {
res.status(500).send(err.message);
}
});
app.post("/api/orders", async (req, res) => {
try {
// use the cart information passed from the front-end to calculate the order amount detals
const { cart } = req.body;
const { jsonResponse, httpStatusCode } = await createOrder(cart);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to create order." });
}
});
app.post("/api/orders/:orderID/capture", async (req, res) => {
try {
const { orderID } = req.params;
const { jsonResponse, httpStatusCode } = await captureOrder(orderID);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to capture order." });
}
});
app.listen(PORT, () => {
console.log(`Node server listening at http://localhost:${PORT}/`);
});This is a breakdown of the /server/server.js code sample.
Declare imports
This section of code imports the dependencies for your Node.js application:
import express from "express";
import fetch from "node-fetch";
import "dotenv/config";node-fetch dependency needed to make http requests to PayPal REST APIs.Set up port and server
This section of code collects the environment variables, sets up the port to run your server, and sets the base sandbox URL:
const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env;
const base = "https://api-m.sandbox.paypal.com";Lines 7-10 start the express Node.js web application framework:
const app = express();
app.set("view engine", "ejs");
app.set("views", "./server/views");
app.use(express.static("client"));Line 8 declares that the rendering engine uses Embedded JavaScript templates.
Generate client token
A client token uniquely identifies your payer. You need a client token for the payer so your app can use the hosted card fields. The following code sample creates a client token for you by making a POST call to the /v1/identity/generate-token endpoint:
/**
* Generate a client token for rendering the hosted card fields.
* See https://developer.paypal.com/platforms/checkout/advanced/sdk/v1/#link-integratebackend
*/
const generateClientToken = async () => {
const accessToken = await generateAccessToken();
const url = `${base}/v1/identity/generate-token`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Language": "en_US",
"Content-Type": "application/json",
},
});
return handleResponse(response);
};This code runs your app on localhost, using the port 8888 that you set up in line 5 of the API call:
app.listen(PORT, () => {
console.log(`Node server listening at http://localhost:${PORT}/`);
});Render checkout page
Set up your app's checkout page to load this section of code when it's rendered by your server:
// render checkout page with client id & unique client token
app.get("/", async (req, res) => {
try {
const { jsonResponse } = await generateClientToken();
res.render("checkout", {
clientId: PAYPAL_CLIENT_ID,
clientToken: jsonResponse.client_token,
});
} catch (err) {
res.status(500).send(err.message);
}
});
app.post("/api/orders", async (req, res) => {
try {
// use the cart information passed from the front-end to calculate the order amount details
const { cart } = req.body;
const { jsonResponse, httpStatusCode } = await createOrder(cart);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to create order." });
}
});
app.post("/api/orders/:orderID/capture", async (req, res) => {
try {
const { orderID } = req.params;
const { jsonResponse, httpStatusCode } = await captureOrder(orderID);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to capture order." });
}
});generateClientToken() function.clientId and clientToken to the page, and inject the value into the script tag.Run createOrder()
This section of code defines a createOrder() callback function for the Create orders endpoint of the Orders v2 API. Call the function when the payment is submitted, for example, when the payer selects the PayPal button, or submits a card payment. See the /client/app.js section for more information.
This createOrder() callback returns a promise that contains the orderID. You can customize this callback for many use cases, such as a physical inventory for a cart, or a digital good with a fixed amount.
Important: To keep your integration secure, don't pass dollar amounts in your finished integration. Define your purchase_units payload on the server side and pass the cart array from the front-end to the server.
/**
* Create an order to start the transaction.
* See /api/orders/v2/orders-create
*/
const createOrder = async (cart) => {
// use the cart information passed from the front-end to calculate the purchase unit details
console.log(
"shopping cart information passed from the frontend createOrder() callback:",
cart,
);
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders`;
const payload = {
intent: "CAPTURE",
purchase_units: [
{
amount: {
currency_code: "USD",
value: "100.00",
},
},
],
};
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
method: "POST",
body: JSON.stringify(payload),
});
return handleResponse(response);
};cart into the createOrder function.url for the API call.intent and purchase_units to an object called payload. This gets passed to the API call.POST request to the Create orders endpoint of the Orders v2 API, using accessToken, url, and the payload from lines 74-84.payload information into JSON format and passes it into the body section of the request.Negative testing for createOrder()
This section of the createOrder() function provides 3 negative testing opportunities behind content tags:
MISSING_REQUIRED_PARAMETERPERMISSION_DENIEDINTERNAL_SERVER_ERRORRemove the // from the beginning of a line to simulate that error. the createOrder() call will pass that line as a PayPal-Mock-Response header in the POST request to the Orders v2 API:
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'Run captureOrder()
This section of code defines the captureOrder() function. Call the function to capture the order and move money from the payer's payment method to the seller:
/**
* Capture payment for the created order to complete the transaction.
* See /api/orders/v2/orders-capture
*/
const captureOrder = async (orderID) => {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders/${orderID}/capture`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
});
return handleResponse(response);
};orderID from the request parameters into the createOrder function.url for the API call, which includes the orderID.POST request to the Capture payment endpoint of the Orders v2 API, using accessToken and url.See /client/app.js in the next section for more details on capturing payments from the front-end.
Negative testing for captureOrder()
This section of the captureOrder() function provides 3 negative testing opportunities behind content tags:
INSTRUMENT_DECLINEDTRANSACTION_REFUSEDINTERNAL_SERVER_ERRORRemove the // from the beginning of a line to simulate that error. The captureOrder() call will pass that line as a PayPal-Mock-Response header in the POST request to the Capture payment endpoint of the Orders v2 API:
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'Test server
Run npm start to confirm your setup is correct.
This command starts the server on localhost:8888. You can specify a port number by modifying line 4 in the sample /server/server.js file.
This breakdown focuses on key lines from the /server/views/checkout.ejs code sample.
Import style sheet
This section calls the JavaScript SDK that defines the PayPal buttons:
<link
rel="stylesheet"
type="text/css"
href="https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css"
/>currency and intent.Add PayPal button
Include the PayPal Checkout button before you request the credit card fields, or on the same page that you request this information. For more information, see Section 8, Required Use of PayPal Checkout, PayPal Credit, in the PayPal Online Card Payments Services Agreement.
<div id="paypal-button-container" class="paypal-button-container"></div>Create your card form
This section of code shows how to create the form fields and Submit button you need to take card payments:
<div class="card_container">
<form id="card-form">
<label for="card-number">Card Number</label>
<div id="card-number" class="card_field"></div>
<div style="display: flex; flex-direction: row">
<div>
<label for="expiration-date">Expiration Date</label>
<div id="expiration-date" class="card_field"></div>
</div>
<div style="margin-left: 10px">
<label for="cvv">CVV</label>
<div id="cvv" class="card_field"></div>
</div>
</div>
<label for="card-holder-name">Name on Card</label>
<input
type="text"
id="card-holder-name"
name="card-holder-name"
autocomplete="off"
placeholder="card holder name"
/>
<div>
<label for="card-billing-address-street">Billing Address</label>
<input
type="text"
id="card-billing-address-street"
name="card-billing-address-street"
autocomplete="off"
placeholder="street address"
/>
</div>
<div>
<label for="card-billing-address-unit"> </label>
<input
type="text"
id="card-billing-address-unit"
name="card-billing-address-unit"
autocomplete="off"
placeholder="unit"
/>
</div>
<div>
<input
type="text"
id="card-billing-address-city"
name="card-billing-address-city"
autocomplete="off"
placeholder="city"
/>
</div>
<div>
<input
type="text"
id="card-billing-address-state"
name="card-billing-address-state"
autocomplete="off"
placeholder="state"
/>
</div>
<div>
<input
type="text"
id="card-billing-address-zip"
name="card-billing-address-zip"
autocomplete="off"
placeholder="zip / postal code"
/>
</div>
<div>
<input
type="text"
id="card-billing-address-country"
name="card-billing-address-country"
autocomplete="off"
placeholder="country code"
/>
</div>
<br /><br />
<button value="submit" id="submit" class="btn">Pay</button>
</form>
<p id="result-message"></p>
</div>card-form object with fields for card-number, expiration-date, cvv, card-holder-name, and billing address information. See the Orders v2 API for details about billing address fields and other parameters. For example, use the 2-character country code to test the billing address.result-message object that displays the output of the resultMessage function from lines 107-110 of the /client/app.js file.Import app.js
This code sample renders the hosted card fields for an eligible payment by importing the /client/app.js file that you created:
<script src="app.js"></script>This section explains how to set up your front end to integrate Expanded Checkout payments.
Front-end process
Front-end code
The /client/app.js file handles the client-side logic and defines how the PayPal front-end components connect with the back-end. Use this file to set up the PayPal checkout using the PayPal JavaScript SDK, and handle the payer's interactions with the PayPal checkout button.
You'll need to save the app.js file in a folder named /client:
async function createOrderCallback() {
try {
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// use the "body" param to optionally pass additional order information
// like product ids and quantities
body: JSON.stringify({
cart: [
{
id: "YOUR_PRODUCT_ID",
quantity: "YOUR_PRODUCT_QUANTITY",
},
],
}),
});
const orderData = await response.json();
if (orderData.id) {
return orderData.id;
} else {
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
}
} catch (error) {
console.error(error);
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
}
async function onApproveCallback(data, actions) {
try {
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const orderData = await response.json();
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message
const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
const errorDetail = orderData?.details?.[0];
// this actions.restart() behavior only applies to the Buttons component
if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else if (
errorDetail ||
!transaction ||
transaction.status === "DECLINED"
) {
// (2) Other non-recoverable errors -> Show a failure message
let errorMessage;
if (transaction) {
errorMessage = `Transaction ${transaction.status}: ${transaction.id}`;
} else if (errorDetail) {
errorMessage = `${errorDetail.description} (${orderData.debug_id})`;
} else {
errorMessage = JSON.stringify(orderData);
}
throw new Error(errorMessage);
} else {
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
resultMessage(
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
);
console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2),
);
}
} catch (error) {
console.error(error);
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${error}`,
);
}
}
window.paypal
.Buttons({
createOrder: createOrderCallback,
onApprove: onApproveCallback,
})
.render("#paypal-button-container");
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message) {
const container = document.querySelector("#result-message");
container.innerHTML = message;
}
// If this returns false or the card fields aren't visible, see Step #1.
if (window.paypal.HostedFields.isEligible()) {
// Renders card fields
window.paypal.HostedFields.render({
// Call your server to set up the transaction
createOrder: createOrderCallback,
styles: {
".valid": {
color: "green",
},
".invalid": {
color: "red",
},
},
fields: {
number: {
selector: "#card-number",
placeholder: "4111 1111 1111 1111",
},
cvv: {
selector: "#cvv",
placeholder: "123",
},
expirationDate: {
selector: "#expiration-date",
placeholder: "MM/YY",
},
},
}).then((cardFields) => {
document.querySelector("#card-form").addEventListener("submit", (event) => {
event.preventDefault();
cardFields
.submit({
// Cardholder's first and last name
cardholderName: document.getElementById("card-holder-name").value,
// Billing Address
billingAddress: {
// Street address, line 1
streetAddress: document.getElementById(
"card-billing-address-street",
).value,
// Street address, line 2 (Ex: Unit, Apartment, etc.)
extendedAddress: document.getElementById(
"card-billing-address-unit",
).value,
// State
region: document.getElementById("card-billing-address-state").value,
// City
locality: document.getElementById("card-billing-address-city")
.value,
// Postal Code
postalCode: document.getElementById("card-billing-address-zip")
.value,
// Country Code
countryCodeAlpha2: document.getElementById(
"card-billing-address-country",
).value,
},
})
.then((data) => {
return onApproveCallback(data);
})
.catch((orderData) => {
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${JSON.stringify(
orderData,
)}`,
);
});
});
});
} else {
// Hides card fields if the merchant isn't eligible
document.querySelector("#card-form").style = "display: none";
}Understand the front-end code
This section explains critical parts of the front-end code sample.
This breakdown focuses on key lines from the /client/app.js code sample.
Create order
This code sample defines the createOrder() function:
async function createOrderCallback() {
try {
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// use the "body" param to optionally pass additional order information
// like product ids and quantities
body: JSON.stringify({
cart: [
{
id: "YOUR_PRODUCT_ID",
quantity: "YOUR_PRODUCT_QUANTITY",
},
],
}),
});
const orderData = await response.json();
if (orderData.id) {
return orderData.id;
} else {
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
}
} catch (error) {
console.error(error);
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
}
async function onApproveCallback(data, actions) {
try {
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const orderData = await response.json();
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message
const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
const errorDetail = orderData?.details?.[0];
// this actions.restart() behavior only applies to the Buttons component
if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else if (
errorDetail ||
!transaction ||
transaction.status === "DECLINED"
) {
// (2) Other non-recoverable errors -> Show a failure message
let errorMessage;
if (transaction) {
errorMessage = `Transaction ${transaction.status}: ${transaction.id}`;
} else if (errorDetail) {
errorMessage = `${errorDetail.description} (${orderData.debug_id})`;
} else {
errorMessage = JSON.stringify(orderData);
}
throw new Error(errorMessage);
} else {
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
resultMessage(
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
);
console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2),
);
}
} catch (error) {
console.error(error);
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${error}`,
);
}
}console.PayPal Button JavaScript
This section calls the JavaScript SDK that defines the PayPal buttons:
window.paypal
.Buttons({
createOrder: createOrderCallback,
onApprove: onApproveCallback,
})
.render("#paypal-button-container");Send results to user
This section declares a resultMessage function that shows a message to the user by passing data to the result-message HTML element container in line 100 of the /server/views/checkout.ejs:
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message) {
const container = document.querySelector("#result-message");
container.innerHTML = message;Lines 108-109 direct the message to the HTML element container.
Check for HostedFields eligibility
Hosted card fields use the JavaScript SDK to help non-PCI-compliant sellers save a payer's card data and create a token for future payments. During an eligible payment, the hosted fields show up in the payment experience and prompt the payer for consent to save their card data. For more information about hosted fields, see Hosted fields.
Note: You need to complete the account setup in sandbox to be eligible for testing hosted fields.
This code sample checks to see if a payment is eligible for hosted card fields. If not, the hosted card fields won't show up during the payment flow:
// If this returns false or the card fields aren't visible, see Step #1.
if (window.paypal.HostedFields.isEligible()) {
// Renders card fields
window.paypal.HostedFields.render(
{...}
} else {
// Hides card fields if the merchant isn't eligible
document.querySelector("#card-form").style = "display: none";
}You can modify this code to show a custom message for eligible or ineligible payments.
Render hosted card fields and create order
This code sample renders the hosted card fields for an eligible payment:
// Renders card fields
window.paypal.HostedFields.render({
// Call your server to set up the transaction
createOrder: createOrderCallback,
styles: {
".valid": {
color: "green",
},
".invalid": {
color: "red",
},
},
fields: {
number: {
selector: "#card-number",
placeholder: "4111 1111 1111 1111",
},
cvv: {
selector: "#cvv",
placeholder: "123",
},
expirationDate: {
selector: "#expiration-date",
placeholder: "MM/YY",
},
},
});createOrderCallback function.selector and placeholder values for the input fields. You can edit this section as needed for your implementation, such as adding more fields. For more information about optional configurations, see Options in the JavaScript SDK reference.Capture eligible payment
This code sample captures the payment when it is eligible for hosted card fields. Add this code to the PayPal button in /client/app.js, right after rendering the card fields:
.then((cardFields) => {
document.querySelector("#card-form").addEventListener("submit", (event) => {
event.preventDefault();
cardFields
.submit({
// Cardholder's first and last name
cardholderName: document.getElementById("card-holder-name").value,
// Billing Address
billingAddress: {
// Street address, line 1
streetAddress: document.getElementById(
"card-billing-address-street",
).value,
// Street address, line 2 (Ex: Unit, Apartment, etc.)
extendedAddress: document.getElementById(
"card-billing-address-unit",
).value,
// State
region: document.getElementById("card-billing-address-state").value,
// City
locality: document.getElementById("card-billing-address-city")
.value,
// Postal Code
postalCode: document.getElementById("card-billing-address-zip")
.value,
// Country Code
countryCodeAlpha2: document.getElementById(
"card-billing-address-country",
).value,
},
})
.then((data) => {
return onApproveCallback(data);
})
.catch((orderData) => {
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${JSON.stringify(
orderData,
)}`,
);
});
});
});POST call in lines 128-131. Anything you pass into the submit is sent to the iframe that communicates with the Orders API. The iframe retrieves the data and sends it along to the POST call. See the Orders v2 API for details about billing address fields and other parameters. For example, use the 2-character country code to test the billing address.onApproveCallback function from lines 38-97, which sends a POST call to /v2/checkout/orders/{id}/capture that captures the order using the orderId in the data object in /server/server.js.Run front end integration
npm start to run your server.localhost:8888.Before going live, test your integration in the sandbox environment.
Learn more about the following resources on the Card Testing page:
Note: Use the credit card generator to generate additional test credit cards for sandbox testing.
If you have fulfilled the requirements for accepting Advanced Credit and Debit Card Payments for your business account, review the Move your app to production page to learn how to test and go live.
If this is your first time testing in a live environment, follow these steps:
Important: The code for the integration checks eligibility requirements, so the payment card fields only display when the production request is successful.
Add security to your checkout integration, or create customizations for your audience.
Payers buy now and pay in installments.
Add the Venmo button to your checkout integration.
Accept local payment methods across the globe.
Add Apple Pay as a payment button.
Authenticate card holders through card issuers.
Captures payment for an order.
Refund all or part of a captured payment.
Reduce declines by getting card updates from the issuer.