How to Use PayPal to Integrate Card Payments to Your Website

Ankur Tiwari
The PayPal Technology Blog
7 min readMar 29, 2022

--

Photo by Karramba Production via Shutterstock

If you are wondering how to get started integrating credit and debit card payments in a Node JS environment, here is a step by step guide and a working demo that shows the server-side and client-side integration needed to get up and running. I’m using NextJS as the framework of choice since it is built on top of React JS and can also have backend API routes to add your server-side logic. I’ll be using react-paypal-js react component, which provides methods and hooks to abstract away the complexity behind loading JS SDK and ensure best practices. To learn more about PayPal hosted fields, check us out on GitHub.

TL;DR — Get to the Demo

Here is the CodeSandbox link if you want to skip reading and jump into the demo — https://codesandbox.io/s/paypal-custom-payment-integration-b9il3r

This integration uses PayPal Sandbox APIs and test credentials, so feel free to play with it and submit a sample payment. You will only be able to make the code changes on your fork. So go for it!

Let’s Get Started!

In NextJS, client and server-side code is organized within the same folder called pages. All of our server routes and function modules are within the pages/api folder and our React components are under the components folder. In this case, I’m using just one React component called <PaymentForm /> , which is initialized in our landing page index.js.

Client Side

First up, create a wrapper <PaymentForm /> component in pages/index.js and pass clientID and clientToken props to it, which are generated on the server side. Next, implement the PaymentForm component and import PayPalScriptProvider, PayPalHostedFieldsProvider, PayPalHostedField, and usePayPalHostedFields.

There are four parts to this integration:

  1. The <PayPalScriptProvider /> provider component that encapsulates the loading complexity and sequence of PayPal JS SDK.
  2. The <PayPalHostedFieldsProvider /> provider component wraps the form field elements and accepts props like createOrder().
  3. The <PayPalHostedField> component is used for the credit card number, expiration, and CVV elements. These are customizable with props and must be children of the <PayPalHostedFieldsProvider /> component.
  4. The usePayPalHostedFields hook exposes the submit() function for submitting the payment with your own custom button.
import { 
PayPalScriptProvider,
PayPalHostedFieldsProvider,
PayPalHostedField,
usePayPalHostedFields
} from "@paypal/react-paypal-js";

Initialize the <PayPalScriptProvider /> component with a client-id and client-token. In order to generate your client-id, you need to create a developeraccount on https://developer.paypal.com/home. If you haven’t done that before, follow the steps by going to the PayPal Developer portal under theGet Credentialssection. As a best practice, do not directly expose your client_secretto the front end. Set it as an environment variable and store it on the server side or in files outside of your application’s source tree.

<PayPalScriptProvider options={{
"client-id": props.clientID,
"data-client-token": props.clientToken,
components: "hosted-fields" }}>

The client-token is generated on the server side by calling PayPal’s Identity API /v1/identity/generate-token, which I will cover in the Server Side section.

export const getServerSideProps = async () => {
const response =
await fetch("https://b9il3r.sse.codesandbox.io/api/tokens");
const data = await response.json();
const { client_token } = data;
const { TEST_CLIENT_ID } = process.env;

return {
props: {
clientToken: client_token,
clientID: TEST_CLIENT_ID
}
};
};

To make a successful transaction in the PayPal online payments world, a merchant needs to first create an order. If the order creation is successful, use the orderId to capture the transaction. So in order to achieve that, define the createOrder function inside the <PayPalHostedFieldsProvider> component, which calls the server-side API route and returns a promise with the OrderId. This OrderId is eventually used in the submitHandler to make the “capture order” API call.

<PayPalHostedFieldsProvider createOrder={
() => {
// Here define the call to create and order
return fetch("/api/payments")
.then((response) =>
response.json())
.then((order) => order.id)
.catch((err) => {
// Handle order creation error
showLoader(false);
showErrorMsg(true);
setErrorMsg(err);
});
}}
>

Now, define the card payment fields for which you will use the PayPalHostedField component to define the card number, CVV and expiration date fields. The PayPalHostedField component is a wrapper on top of HTML form fields to make them more secure, and restricts a hacker from tampering with them using malicious JS.

<PayPalHostedField id=”card-number” hostedFieldType=”number”. options={{ selector: “#card-number”, placeholder: “4111 1111 1111 1111” }} className={styles.card_field} />

Next, use the usePayPalHostedFields hook in asubmitPayment component to wire up the card payment form submit functionality with the PayPal provided submit handler. This function accepts an object as an arg, where you can pass some additional card details like cardholder name and billing address, and returns an order object. In the success callback of submit function, you get the orderId and pass it to the payment capture call, which then returns the transaction data including transaction id and status. These can be used further in the payment flow. And that’s it! That’s all you need for the client side.

const SubmitPayment = () => {
// Here declare the variable containing
// the hostedField instance
const { cardFields } = usePayPalHostedFields();
const submitHandler = () => {
if (typeof cardFields.submit !== "function") return;
// validate that `submit()` exists before using it
showLoader(true);
showSuccess(false);
cardFields.submit({
// The full name as shown in the card and billing address
cardholderName: "John Wick"
}).then((order) => {
const { orderId } = order;
fetch(`/api/payments/${orderId}`)
.then((response) => response.json())
.then((data) => {
showLoader(false);
showSuccess(true);
setTransactionData(data);
// Inside the data you can find all the information related to the payment
})
.catch((err) => {
// Handle any error
});
})
.catch((err) => {
// Handle capture order error
showLoader(false);
showErrorMsg(true);
setErrorMsg(err);
});
};

return (
<button onClick={submitHandler} className="btn btn-primary">
Pay</button>
);
};

Server Side

Now let’s jump into the server-side implementation. As you might have already figured out by now, we are making three different API calls:

  1. Client Token generation - /v1/identity/generate-token
  2. Create Order - /v2/checkout/orders
  3. Payment Capture - /v2/checkout/orders/:orderId/capture

There is one additional call you need to make which is common to all these three calls - generateAccessToken

export const generateAccessToken = async () => {
const { TEST_CLIENT_ID, TEST_CLIENT_SECRET } = process.env;
const { AUTH_API_URL } = sandboxURLs;
const encodedToken =
Buffer.from( TEST_CLIENT_ID + ":" + TEST_CLIENT_SECRET)
.toString("base64");
const response = await fetch(AUTH_API_URL, {
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded",
Accept: "application/json",
Authorization: "Basic " + encodedToken
},
body: "grant_type=client_credentials"
});
const { access_token } = await response.json();
return access_token;
};

Log in to your Developer Dashboard (explained previously) and get your client_id and client_secret. In a production environment, store them in environment variables or in files outside of your application’s source tree. That being said, it is safe to store your client_id in a git file or HTML source code but client_secret should be handled differently. In this example, I’m storing both of them as my environment variables. The above code sample explains how you can use your client_id and client_secret to generate an access token using the /v1/oauth2/token endpoint. This access token is passed in the Authorization header for the above three API calls you need to make.

One thing to notice in thecreateOrder call, you are building the payload on the server side without relying on the data passed from the front end. This is a reliable way of retrieving and validating the amount, currency and purchase units on the server side by calling another service or using the session object.

const createOrder = async (req) => {
const { ORDERS_API_URL } = sandboxURLs;
const accessToken = await generateAccessToken();
const payload = {
intent: "CAPTURE",
// change this block and dynamically get the order amount from session
purchase_units: [{
amount: {
currency_code: "USD",
value: "10.00"
}
}]
};
const response = await fetch(
ORDERS_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + accessToken
},
body: JSON.stringify(payload)
});
const data = await response.json();
console.log(`Create Order Call- ${JSON.stringify(data)}`);
return data;
};

Full Demo - https://codesandbox.io/s/paypal-custom-payment-integration-b9il3r

Conclusion

I hope this explains how to integrate PayPal custom card checkout into a web application and build a custom payment flow needed for your business. Feel free to reach out to us if you have any further questions - https://twitter.com/paypaldev

Happy coding 🙌

--

--