Imagine you've spent weeks building the perfect payment flow with a payment service provider like Flutterwave. Your code is clean, the UI is smooth, and everything works in your development environment.
Then comes the pre-launch security review, and suddenly you're scrambling to fix PCI compliance issues that could have been avoided from day one.
This blog will show you exactly how to build PCI compliance into your development workflow from the start, avoiding those costly last-minute fixes and security vulnerabilities that keep developers up at night.
By the end of this blog, you’ll know:
- How to properly handle test data without creating security risks
- Practical coding patterns that satisfy PCI requirements while you build
What is PCI-DSS?
PCI-DSS is the reason you can’t just log a card number and call it a day. PCI-DSS is a set of security rules established by major credit card companies to protect cardholder data. What this means is that you should be concerned about PCI-DSS if your application touches payment card information in any way.
The body that manages the PCI-DSS standard is called the PCI Security Standards Council (PCI SSC).
Why bother with PCI compliance in dev/staging?
Since most payment service providers are already PCI DSS compliant, why do you, as a developer, still need to be concerned about it, particularly in your development and staging environments? The answer lies in understanding that PCI compliance is a comprehensive standard that applies to the entire ecosystem handling card data. While your provider (e.g., Flutterwave) secures its own platform and processes, the way your application interacts with that provider, and any card data it might touch or influence, falls under your control.
Here's why it's still important:
- Vulnerabilities can start in dev: A flaw introduced during development (e.g., insecure handling of test data that mimics production data flows or insecure coding practices) can inadvertently create risks.
- Dev environments can be targets: Less secure development setups can be entry points if not properly managed.
- Integration Matters: How your application integrates with Flutterwave directly impacts the scope of PCI Compliance for the merchant you're developing for and, by extension, how you should design and test your code.
- Costly last-minute fixes: Addressing PCI Compliance issues late is always more painful.
Merchant refers to any individual or business that sells goods or services and uses Flutterwave's platform to accept and process payments from their customers.
Key PCI Compliance terms to know
To help you follow along and navigate the landscape of PCI compliance, here are some key terms you should know:
- PCI-DSS: Payment Card Industry Data Security Standard.
- PAN (Primary Account Number): The main number on a credit or debit card.
- Cardholder Data: Includes PAN, cardholder name, expiry date, and service code. Sensitive authentication data (CVV, PINs) should never be stored after authorization.
- CDE (Cardholder Data Environment): Systems, networks, and processes that store, process, or transmit cardholder data or could impact its security. Your goal in dev/test is often to ensure your app components stay out of the CDE as much as possible, or if they are in-scope (like in a direct charge scenario), they are handled with extreme care, even with test data.
- Mock/Synthetic Data: Essential for testing.
- Tokenization: Replacing sensitive card data with non-sensitive, unique identifiers. Learn more about tokenization in secure payment gateways.
- SAQ (Self-Assessment Questionnaire): A tool merchants use to validate their PCI Compliance. The type of SAQ depends heavily on how they handle card data (which is influenced by your integration).
How to pass PCI-DSS in your dev environment
Developers don't typically get their development environment itself formally "PCI DSS checked" or certified in the same way a live production environment would be. PCI DSS compliance applies to systems that store, process, or transmit actual cardholder data, which should be your production environment.
However, developers aim to build and test their applications in the development environment so that when deployed to production, the resulting application supports the merchant's PCI DSS compliance.
Here’s how developers can work towards this in their dev environment when using Flutterwave:
1. Understand your PCI-DSS scope based on integration
The first and most crucial step is to understand how your chosen Flutterwave integration method impacts the PCI DSS scope:
For Direct Card Charge
Suppose you're using an API integration where your application directly collects, transmits, or could impact the security of full cardholder data (PAN, CVV, etc.), the merchant's systems (including your application) will have significant PCI DSS responsibilities. Flutterwave's documentation (on Direct Card Charge integration) notes that the merchant requires "a PCI DSS compliance certificate" for direct card charges. Your dev environment should then be geared towards building an application that can meet these stringent requirements in production.
Hosted Solutions (Flutterwave Inline, Standard Checkout, Payment Links)
If you use Flutterwave's hosted payment pages, iframes (like Flutterwave Inline), or Payment Links, sensitive cardholder data is entered directly into Flutterwave's secure, PCI DSS-compliant environment, not yours. This means your server doesn't directly touch the raw card details, significantly reducing your PCI DSS scope.
Here's a practical look at how this works with an example using Flutterwave Inline:
// you include the Flutterwave Inline library with a script tag
<script src="https://checkout.flutterwave.com/v3.js"></script>
// Example function to trigger payment on a button click
function makePayment() {
FlutterwaveCheckout({
public_key: "YOUR_FLUTTERWAVE_PUBLIC_KEY", // Use your TEST public key in dev
tx_ref: "YOUR_UNIQUE_TRANSACTION_REFERENCE", // Generate a unique ref for each transaction
amount: 100, // Amount in the currency specified
currency: "NGN", // Or any other supported currency
payment_options: "card, banktransfer, ussd", // Specify allowed payment options
redirect_url: "https://your-website.com/payment-callback", // Your callback URL
customer: {
email: "customer-email@example.com",
phone_number: "08012345678",
name: "Customer Name",
},
customizations: {
title: "My Awesome Store",
description: "Payment for goods",
logo: "https://your-website.com/logo.png",
},
// In a real scenario, you'd handle the response in your callback URL
// For testing in dev, you might log the response or use Flutterwave's test environment features
callback: function (data) {
console.log("Payment successful:", data);
// Here you would typically redirect to your server-side verification URL
// window.location.href = `https://your-website.com/payment-callback?transaction_id=<span class="math-inline">\{data\.transaction\_id\}&tx\_ref\=</span>{data.tx_ref}`;
},
onclose: function() {
// Handle case where user closes the modal
console.log("Payment modal closed.");
}
});
}
From the code above, you can see that your application code (JavaScript here) only passes transaction references, amounts, and customer details. It does not handle the raw card number, CVV, or expiry date. Flutterwave handles that within its secure environment.
2. Do not use live card data in test environments!
If you use Flutterwave's direct card charge, your systems will handle sensitive card data. This means the merchant (your client) needs their own PCI DSS compliance certificate for their production environment. You can build the forms and backend logic in your dev environment, but ONLY EVER use Flutterwave's provided test card numbers. This is important for preventing accidental real charges, avoiding severe security risks associated with handling live card data in less secure environments, and maintaining PCI DSS compliance by keeping actual cardholder data out of non-production systems.
For instance, your front-end might include a form like this to collect card details:
// frontend form (do not pass real card data unless you're PCI-DSS compliant)
<form id="direct-charge-form">
<div>
<label for="card-number">Card Number (TEST DATA ONLY)</label>
<input type="text" id="card-number" name="cardNumber" placeholder="xxxx xxxx xxxx xxxx">
</div>
<div>
<label for="expiry-month">Expiry Month (TEST DATA ONLY)</label>
<input type="text" id="expiry-month" name="expiryMonth" placeholder="MM">
</div>
<div>
<label for="expiry-year">Expiry Year (TEST DATA ONLY)</label>
<input type="text" id="expiry-year" name="expiryYear" placeholder="YY">
</div>
<div>
<label for="cvv">CVV (TEST DATA ONLY)</label>
<input type="text" id="cvv" name="cvv" placeholder="xxx">
</div>
<button type="submit">Pay (TEST)</button>
</form>
When using direct card charges, the payload containing card details often needs to be encrypted before sending to Flutterwave. For example, take this code:
// EXAMPLE FOR DEV USING TEST CARD DETAILS
// This assumes you are collecting card details on your server-side
// (which significantly increases PCI scope in production).
// In a real direct charge scenario in production, this backend would need to be
// within a PCI DSS certified environment.
app.post("/charge-direct", async (req, res) => {
const { cardNumber, expiryMonth, expiryYear, cvv, amount, email, tx_ref } =
req.body;
// **CRITICAL: IN A DEV ENVIRONMENT, cardNumber, expiryMonth, expiryYear, cvv MUST BE TEST VALUES**
// **NEVER LOG OR STORE THESE VALUES UNENCRYPTED, EVEN IN DEV (except for temporary debugging with test data)**
try {
const flw = new Flutterwave(
"YOUR_FLUTTERWAVE_PUBLIC_KEY",
"YOUR_FLUTTERWAVE_SECRET_KEY"
);
const payload = {
card_number: cardNumber, // TEST CARD NUMBER
cvv: cvv, // TEST CVV
expiry_month: expiryMonth, // TEST EXPIRY
expiry_year: expiryYear, // TEST EXPIRY
currency: "NGN",
amount: amount,
email: email,
tx_ref: tx_ref,
// May require PIN, AVS, or redirect for 3DS depending on the card [4]
// authorization: { mode: 'pin', pin: 'TEST_PIN' } // Example
};
// The SDK would encrypt and send this.
const response = await flw.Charge.card(payload);
// Process response: handle 3DS redirects, PIN requests, or success/failure
res.json(response);
} catch (error) {
console.error("Direct charge error:", error);
res.status(500).json({ message: "Payment processing error" });
}
});
As a developer building this, even in your test environment:
- You must be hyper-aware of secure coding practices for any component that would handle this data.
- Your test data, while not real PANs, should still be handled with care as if it were sensitive to ensure your data flow logic is secure.
3. Secure API key management
Never hardcode API keys. Use environment variables or a secret manager. Hardcoding keys directly in your source code creates serious security vulnerabilities, such as exposure in version control systems or accidental leaks, making managing or rotating keys difficult.
// In your.env file (add this to.gitignore)
FLUTTERWAVE_PUBLIC_KEY_TEST=your_test_public_key
FLUTTERWAVE_SECRET_KEY_TEST=your_test_secret_key
// In your application code
require('dotenv').config(); // If using the dotenv package
const publicKey = process.env.FLUTTERWAVE_PUBLIC_KEY_TEST;
const secretKey = process.env.FLUTTERWAVE_SECRET_KEY_TEST;
const flw = new Flutterwave(publicKey, secretKey);
4. Validate your inputs (Never trust the user)
All data from external sources (like a user's browser, a mobile app, or another internal service) must be treated as potentially malicious or malformed until proven otherwise through some validation. This is especially true for payment information, and you should integrate validation logic for all payment data into your codebase.
// PAN Validation (example)
if (!cardNumber || !/^[0-9]{13,19}$/.test(cardNumber)) {
errors.push("Invalid test card number format (must be 13-19 digits).");
}
// Add Luhn check for test cards if applicable
// Expiry Month Validation
const month = parseInt(expiryMonth, 10);
if (isNaN(month) || month < 1 || month > 12) {
errors.push("Invalid test expiry month (must be 01-12).");
}
// Expiry Year Validation (basic)
const year = parseInt(expiryYear, 10); // Assuming YY or YYYY format
const currentYearFull = new Date().getFullYear();
let inputYearFull = year;
if (year < 100) {
// Handles YY format
inputYearFull = 2000 + year;
}
const currentMonth = new Date().getMonth() + 1;
if (
isNaN(year) ||
inputYearFull < currentYearFull ||
(inputYearFull === currentYearFull && month < currentMonth)
) {
errors.push("Test card is expired or invalid year.");
}
// CVV Validation
if (!cvv || !/^[0-9]{3,4}$/.test(cvv)) {
errors.push("Invalid test CVV format (must be 3 or 4 digits).");
}
If you prefer not to write extensive manual validation code as shown above, or if you need more complex validation, you should use a validation library like Joi, express-validator, or whatever validation library your preferred programming language allows.
For comprehensive PCI compliance, you can always refer to the latest official standards from the PCI Security Standards Council (PCI SSC).
Wrapping up
In this blog, you learned how compliance is a collaborative effort between you as the developer and Flutterwave. By understanding your integration's impact on the PCI DSS scope, diligently using test data and keys, securing your API credentials, and validating every input, you’re laying a strong foundation for a secure payment solution with Flutterwave.
Remember that while Flutterwave provides a secure and compliant platform, the security of the end-to-end transaction also depends on the practices you implement in your application.
If you’re ready to build a secure payment experience, check out Flutterwave today.
Top comments (0)