As the demand for online payment options increases, card payments have quickly proven to spike the interest of most customers. Their appeal lies in their speed, accessibility, and security.
Unlike some payment methods, which require multiple steps for bank approval or third-party verification (like Automated Clearing House (ACH) transfers or wire transfers), card payments offer a more direct process. Once the issuing bank authorizes a card payment, it is then processed almost immediately, without the need for additional approval, reducing the delay in completing the transaction.
Flutterwave facilitates various payment methods, including card charges, bank transfers, and mobile money. Flutterwave’s card payment option provides a reliable and secure platform for handling card payments, fully compliant with Payment Card Industry Data Security Standard (PCI DSS). This makes collecting payments seamless, ensuring a smooth customer experience.
In this tutorial, you’ll learn how to integrate Flutterwave’s direct card payment into your web application. You’ll also learn about the available card options and understand their differences. Finally, you’ll review the best practices for making online card payments.
Prerequisites
Before delving into integration, you should have the following:
- Experience with building server-side apps with Nodejs.
- Experience with HTML, CSS, and Javascript.
- A good understanding of how payment gateways work.
- A Flutterwave account.
- The node package manager
npm
installed on your device. If you don't have one, here’s how to install it.
Let's look at the basic steps to follow when integrating.
Basic Steps to Integrating Card Payment
There are 3 main steps to keep in mind when integrating Flutterwave direct card charges:
-
Initiating the Charge: This is the initial request, where you will send the card details (Card number, expiry date, CVV). The request will also include other required payloads like:
-
currency
- The currency for the transaction (e.g., "NGN") -
amount
- The amount to charge -
email
- Customer's email address -
full_name
- Customer's full name -
phone_number
- Customer's phone number -
enckey
- Your Flutterwave encryption key -
tx_ref
- A unique transaction reference
An encryption key is needed to make this request. Encrypting sensitive card details enhances the security of the transaction data being sent.
-
-
Completing the Charge: Depending on the type of card passed in the previous request, you’ll get a response from Flutterwave regarding the next step. Here are the four possible card scenarios you can expect and the next steps for these cards.
a. PIN Cards: These are cards that require a PIN for authorization.
- Next Step: First, collect the card PIN from the customer and send it with the next request. Then, the user will be required to enter an OTP to complete the payment.
b. 3DS/VBV Cards: These cards use 3D Secure or Verified by Visa authorization.
- Next Step: Redirect the customer to their bank page to complete the charge.
c. No Authentication: These card types require completing the charge with the user’s PIN.
- Next Step: Collect the card PIN from the customer and send it with the next request.
d. Address Verification Service (AVS): These cards require the user’s billing address for authentication.
- Next Step: Collect the billing address details from the customer and send it with the next request.
Validate the Charge: This is the final step to completing the charge. It is mainly applicable to the PIN cards. You’ll need to send in a one-time password to validate and complete the card payment.
Now that you understand the basic steps for mocking different cards let’s create a work environment to integrate them.
Setting Up Work Environment
-
First, go to your preferred IDE and run the command below to create a new directory.
mkdir direct-charge cd direct-charge
-
Next, run the command below to initiate a node project.
npm init -y
-
Add a
.env
and aserver.js
file to the directory with the command.
touch server.js .env
-
Go to your Flutterwave dashboard and navigate to the Settings > API key section to get your API keys. Copy and paste them into the variables below in your
.env
file.
FLW_PUBLIC_KEY=<YOUR_PUBLIC_KEY_HERE> FLW_SECRET_KEY=<YOUR_SECRET_KEY_HERE> FLW_ENCRYPTION_KEY=<YOUR_ENCRYPTION_KEY_HERE>
-
Finally, run the command below to install the following packages.
npm install cors dotenv express Flutterwave-node-v3 nodemon
Let’s set up your node server.
Setting Up Integration Server
Now that you have the essential setup ready and understand how the payment flow works, let’s integrate Flutterwave’s direct card charges using the Flutterwave Node.js SDK.
-
Navigate to the
server.js
file and add the code below to initialize your backend server.
// server.js const express = require('express'); const Flutterwave = require('Flutterwave-node-v3'); const cors = require('cors'); const bodyParser = require('body-parser'); const app = express(); const port = process.env.PORT || 3000; const dotenv = require('dotenv'); dotenv.config(); const flw = new Flutterwave(process.env.FLW_PUBLIC_KEY, process.env.FLW_SECRET_KEY); app.use(cors()); app.use(express.json()); app.use(bodyParser.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(__dirname)); // Endpoint for rendering static HTML form app.get('/', (req, res) => { res.sendFile(__dirname + './index.html'); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });
The code above imports and sets up the packages you’ll need to build your integration server, including the
Flutterwave-node-v3
package. The first endpoint here will be responsible for rendering the HTML form you’ll create to simulate your payment flow. -
Next, set up a
/charge-card
endpoint to initialize the card payment. The endpoint initializes the card payment by calling theflw.Charge.card
method. It handles the various flows that Flutterwave requires to send the next step parameters. Add the code below:
// server.js // Endpoint for rendering static HTML form // Handle card charge app.post('/charge-card', async (req, res) => { new_ref = "example" + Date.now(); const payload = { card_number: req.body.card_number, cvv: req.body.cvv, expiry_month: req.body.expiry_month, expiry_year: req.body.expiry_year, currency: "NGN", amount: "100", email: req.body.email, fullname: req.body.fullname, phone_number: req.body.phone_number, enckey: process.env.FLW_ENCRYPTION_KEY, tx_ref: new_ref, }; new_payload = payload; try { const response = await flw.Charge.card(payload); if (response.meta.authorization.mode === 'pin') { res.json({ status: 'pin_required', }); } else if (response.meta.authorization.mode === 'redirect') { res.json({ status: 'redirect_required', url: response.meta.authorization.redirect, tx_ref: new_ref }); } else if (response.meta.authorization.mode === 'otp') { res.json({ status: 'otp_required', tx_ref: new_ref, flw_ref: response.data.flw_ref }); } else { res.json({ status: 'success', data: response.data }); } } catch (error) { console.error(error); res.status(500).json({ status: 'error', message: error.message }); } }); // app.listen(port, () => {
If the card details sent in the request are a PIN Card or a 3DS Card, Flutterwave returns a payload telling you what to send to authorize the charge.
-
Next, create a
/complete-charge
endpoint that calls Flutterwaveflw.Charge.card
method, but this time, passing in the authorization required by Flutterwave to authorize the charge.
// server.js // Endpoint to Handle card charge // Handle additional authorization app.post('/complete-charge', async (req, res) => { const { authMode, pin } = req.body; try { let response; if (authMode === 'pin') { response = await flw.Charge.card({ ...new_payload, authorization: { mode: 'pin', pin: pin } }); } if (response.status === "success") { res.json({ status: 'success', data: response }); } else { res.json({ status: 'failed', message: response.data.processor_response }); } } catch (error) { console.error(error); res.status(500).json({ status: 'error', message: error.message }); } });
-
Add an endpoint to validate the transaction, which will call the
flw.Charge.validate
method passing in an OTP to complete the card charge.
// server.js // Endpoint to Handle additional authorization // Validates the card charge app.post('/validate-charge', async (req, res) => { try { const { otp, flw_ref } = req.body; const response = await flw.Charge.validate({ otp: otp, flw_ref: flw_ref }); res.json(response); } catch (error) { res.status(500).json({ error: error.message }); } });
-
Finally, run the command below to run the server. Use nodemon for this, so your server keeps running while you make changes.
nodemon server.js
Now that you have your server-side integration set up, let’s create a front-end form for users to enter their card details before making requests to Flutterwave.
Implementing the Payment Form
-
Create the following files
index.html
andstyles.css
,
by running the command below:
touch index.html styles.css
Head over to GitHub to copy and add the CSS script to your
styles.css
file.-
Now, let's create a basic HTML form for users to input their card details and the additional authorization parameters (PIN and OTP) required to authorize the card payment. You’ll only display the field for these inputs after the first response is sent, and you will know the type of card you’re charging and the parameters to send next.
/* index.html */ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="./styles.css"/> <title>Flutterwave Card Payment</title> </head> <body> <div class="container"> <h1>Flutterwave Card Payment</h1> <form id="paymentForm"> <input type="text" name="card_number" placeholder="Card Number" > <input type="text" name="cvv" placeholder="CVV" > <input type="text" name="expiry_month" placeholder="Expiry Month" > <input type="text" name="expiry_year" placeholder="Expiry Year" > <input type="email" name="email" placeholder="Email" > <input type="text" name="fullname" placeholder="Full Name" > <input type="tel" name="phone_number" placeholder="Phone Number" > <button type="submit">Pay Now</button> </form> <div id="authForm" style="display: none;"> <h2>Additional Authorization Required</h2> <form id="additionalAuthForm"> <div id="pinAuth" style="display: none;"> <input type="password" name="pin" placeholder="Enter PIN" > </div> <div id="otpAuth" style="display: none;"> <input type="text" name="otp" placeholder="Enter OTP" > </div> <button type="submit">Complete Payment</button> </form> </div> <div id="response"></div> </div> <script></script> </body> </html>
Now that your server is running, navigate to the port
http://localhost:3000/
on your browser, and you should see an image like the one below.Next, you’ll create the flow to simulate multiple scenarios.
Note: For this tutorial, you’ll focus on simulating the PIN, 3DS, and No-Authorization card scenarios.
Head over to Flutterwave’s testing page to get card details to mock the multiple card scenarios you’ll cover.
Handling Payment Flow
Over the next few steps, you’ll simulate the payment flow for the multiple card scenarios (PIN, No Auth, and 3DS).
Step 1: Initialize Variable
Create the global variables to capture new responses from Flutterwave and use them across multiple endpoints.
<!-- index.html -->
</div>
<script>
let currentFlwRef = '';
let currentAuthMode = '';
let currentTxRef = '';
</script>
</body>
currentFlwRef
: Stores the current Flutterwave reference for the transaction.currentAuthMode
: Tracks the current authentication mode (e.g., 'pin', 'OTP').currentTxRef
: Stores the current transaction reference.
Step 2: Handle Form Submission
Attach an event listener to the form with the ID paymentForm
to enable asynchronous handling of the form submission. When the form is submitted, you’ll use a handleChargeResponse()
function to handle the authorization for the next step.
<!-- index.html -->
<script>
...
// Step 1 code goes here
...
document.getElementById('paymentForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const payload = Object.fromEntries(formData);
try {
const response = await fetch('/charge-card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
handleChargeResponse(result);
} catch (error) {
console.error('Error:', error);
}
});
</script>
Step 3: Handle Payment Response
Now, let’s create a handleChargeResponse
function to handle the response you get from Flutterwave depending on the type of card used to initiate the transfer so you’ll know what authorization parameters to send.
/* index.html */
<script>
...
// Step 2 code goes here
...
function handleChargeResponse(result) {
if (result.status === 'pin_required') {
currentFlwRef = result.flw_ref;
currentTxRef = result.tx_ref;
currentAuthMode = 'pin';
showAuthForm(currentAuthMode);
} else if (result.status === 'redirect_required') {
// Automatically redirect to the OTP page
window.location.href = result.url;
} else if (result.status === 'success') {
document.getElementById('response').innerText = 'Payment successful!';
} else {
document.getElementById('response').innerText = 'Payment failed. Please try again.';
}
}
</script>
Step 4: Show Authorization Form
After sending the initial request /charge-card
and getting a response from Flutterwave, depending on the type of card details passed, you’ll display the additional form to the user, either to pass in their card PIN for PIN cards or redirect them to their bank or an OTP to validate and complete the charge.
/* index.html */
<script>
...
// Step 3 code goes here
...
function showAuthForm(authMode) {
document.getElementById('authForm').style.display = 'block';
document.getElementById('pinAuth').style.display = authMode === 'pin' ? 'block' : 'none';
document.getElementById('otpAuth').style.display = authMode === 'otp' ? 'block' : 'none';
}
</script>
Step 5: Handle Additional Authorization Form Submission
When the additionalAuthForm
is submitted, the code prevents the default behaviour of handling the submission. It collects the form data and checks the currentAuthMode
to determine whether the request should proceed with PIN or OTP verification.
/* index.html */
<script>
...
// Step 4 code goes here
...
// Handle Additional Authorization Form Submission
document.getElementById('additionalAuthForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const authData = Object.fromEntries(formData);
try {
if (currentAuthMode === 'pin') {
// Handle PIN submission in the next step
} else if (currentAuthMode === 'otp') {
// Handle OTP submission
const response = await fetch('/validate-charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
otp: authData.otp,
flw_ref: currentFlwRef
})
});
const result = await response.json();
if (result.status === 'success') {
document.getElementById('response').innerText = 'Payment successful!';
document.getElementById('authForm').style.display = 'none';
} else {
document.getElementById('response').innerText = 'OTP validation failed. Please try again.';
}
}
} catch (error) {
console.error('Error:', error);
document.getElementById('response').innerText = 'An error occurred. Please try again.';
}
});
</script>
Step 6: Handle PIN Response
If the currentAuthMode
is set to pin
, a request is sent to flw.Charge.validate
with the entered PIN. The response is checked for success:
/* index.html */
<script>
...
// Step 5 code goes here
...
if (currentAuthMode === 'pin') {
const response = await fetch('/complete-charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
authMode: 'pin',
pin: authData.pin,
})
});
const result = await response.json();
currentFlwRef = result.data.data.flw_ref;
currentTxRef = result.data.data.tx_ref;
if (result.status === 'success' && result.data.data.status === 'pending') {
currentAuthMode = 'otp';
showAuthForm('otp');
} else if (result.status === 'success' && result.data.data.status === 'successful') {
document.getElementById('response').innerText = 'Payment successful!';
document.getElementById('authForm').style.display = 'none';
} else {
document.getElementById('response').innerText = 'PIN validation failed. Please try again.';
}
}
</script>
-
If pending, the
currentAuthMode
is updated tootp
, prompting the user for OTP verification.
- If successful, a success message is displayed like in the image below.
- If validation fails, an error message prompts the user to re-enter the PIN.
You can also check out the complete demo app on GitHub.
Wrapping Up
Now that you’ve gone through the steps to integrate Flutterwave card payment into your website using the Flutterwave Node SDK, you can begin accepting seamless card payments from your customers.
With this integration, you’ve added a key feature that meets the demands of today’s economy, setting your business up for growth and customer satisfaction.
Check out our Node.js SDK for more payment methods you can integrate. You can also check out our developer documentation on best practices.
Read more on how to integrate a payment gateway into your e-commerce app.
Top comments (0)