Why the integration layer is the weakest link
Payment gateways possess PCI-DSS Level 1 certification. Their infrastructure is hardened. They employ dedicated red teams. They monitor traffic patterns globally. But the moment you write code that interacts with their API, you assume all the risk. Initialising transactions, verifying callbacks, processing webhooks. You inherit responsibility for everything that happens on your side of the boundary. The shared responsibility model dictates this. The gateway secures the platform. You secure your integration. Most critical payment vulnerabilities we discover during API security assessments do not exist in the gateway. They live entirely in your integration code.
Nigerian fintechs move fast. Payment integration often represents the very first feature shipped. Speed pressure forces developers to cut critical corners. Webhook signatures go unchecked. Amounts are trusted straight from the client. Secret keys leak into frontend JavaScript bundles. Each of these mistakes forms a direct, exploitable path to catastrophic financial loss. Attackers scan for these exact flaws daily. They automate the discovery of exposed endpoints. They reverse engineer mobile apps to extract hardcoded API keys. They replay webhook payloads to multiply their balance arbitrarily.
You must treat every incoming request as hostile. You cannot trust the client. You cannot trust the network. You cannot even trust the gateway payload without cryptographically verifying its origin. State machine manipulation is rampant. A payment transitions from pending to successful. What happens if an attacker forces a transition from failed to successful? Or pending to refunded? Your state machine must enforce strict, valid transitions. If a payment is already marked failed, a late webhook claiming successful should trigger an immediate manual review. It should never trigger an automated database overwrite. If you fail to secure payment apis in fintech startups, you will lose money. Period. Attackers weaponise your codebase against you.
Attackers exploit mass assignment vulnerabilities during payment initialisation. If your backend takes the entire JSON payload from the client and binds it directly to the database model, you fail. The attacker injects fields like is_admin=true or payment_status=successful directly into the initialisation request. If your ORM blindly accepts these fields, the attacker bypasses the gateway entirely. They credit themselves without ever visiting the checkout page. You must strictly define permitted parameters. Whitelist only the fields you explicitly require. Drop everything else.
Webhook signature verification: the non-negotiable
Paystack signs every webhook payload with HMAC-SHA512 using your secret key. They place the resulting hash in the x-paystack-signature header. Flutterwave utilises a verif-hash header containing a secret hash you define in your dashboard. Stripe uses HMAC-SHA256 with a dedicated webhook signing secret placed in the stripe-signature header. Despite the different naming conventions and hashing algorithms, the core verification pattern remains identical. You must validate the cryptographic signature before executing a single line of business logic.
Failing to perform this check allows anyone on the internet to forge payment events. An attacker can craft a JSON payload claiming a successful payment of one million Naira. They point a simple curl command at your webhook URL. If you do not verify the signature, your system processes the fake payment. It updates the user's wallet balance. Your company absorbs the complete financial loss. A signature proves the gateway sent the payload. It acts as your primary defense against external spoofing.
Webhooks also introduce Server-Side Request Forgery (SSRF) risks. Some gateways allow you to specify dynamic webhook URLs in the initialisation payload. If an attacker controls this URL parameter on the frontend, they point it at your internal infrastructure. They force your payment provider to scan your internal network. They target internal Redis instances or AWS metadata endpoints. Always hardcode your webhook URLs in the gateway dashboard. Never pass them dynamically from the client. Never trust a URL provided by an untrusted source.
The three-step verification pattern
First, capture the raw request body exactly as it arrives over the network. You must do this before any JSON parsing occurs. This step is critical and frequently mishandled. Frameworks like Express.js automatically parse JSON. They strip trailing whitespace. They reorder dictionary keys. Any micro-modification to the payload alters the cryptographic hash. You must compute the HMAC against the exact raw bytes sent by the gateway. Use middleware to stash the raw buffer.
Second, compute the HMAC. Use the raw body buffer and your secret key. Select the correct algorithm. Use SHA-512 for Paystack. Use SHA-256 for Stripe. The output must match the gateway's encoding format, typically hexadecimal.
Third, compare the computed hash against the header value. You must use a constant-time comparison function. In Node.js, use crypto.timingSafeEqual(). Never use a naive string comparison like ===. Standard equality operators leak timing information. An attacker exploits this byte by byte. They measure the microsecond difference in response times. They guess the hash. If the first character matches, the comparison takes slightly longer before failing on the second character. They iterate this process until they forge a valid signature entirely from the outside.
const crypto = require('crypto');
function verifyWebhookSignature(req, res, next) {
const signatureHeader = req.headers['x-paystack-signature'];
const secretKey = process.env.PAYSTACK_SECRET_KEY;
// Compute HMAC using the raw request buffer. Never use parsed JSON.
const hash = crypto.createHmac('sha512', secretKey)
.update(req.rawBody)
.digest('hex');
const hashBuffer = Buffer.from(hash);
const signatureBuffer = Buffer.from(signatureHeader);
if (hashBuffer.length !== signatureBuffer.length) {
return res.status(401).send('Invalid signature length');
}
// Prevent timing attacks using constant-time comparison
if (!crypto.timingSafeEqual(hashBuffer, signatureBuffer)) {
return res.status(401).send('Invalid signature');
}
next();
} We covered the full verification flow in our webhook security deep-dive. The critical takeaway remains: signature verification must act as an ironclad middleware. Do not bury it deep inside a controller function. New routes and future refactoring must never be able to bypass it accidentally.
Client-side amount trust
In over 40% of the fintech pentests we conduct, the transaction amount sent from the frontend is trusted blindly. The server accepts it without re-verification. An attacker intercepts the request using tools like Burp Suite. They initialise a ₦100 transaction. They manipulate the client payload to report ₦100,000. They intercept the callback. The system credits the massive amount. Always re-verify the amount against your database record after the gateway confirms payment. Never trust the frontend. The frontend is a hostile environment. Attackers control it completely. They bypass your JavaScript validations effortlessly. You must enforce security on the backend.
Server-side amount validation
After receiving a successful webhook or a redirect callback, your backend must perform a strict verification API call. You must call the gateway's verification endpoint directly from your server. For example, execute a GET /transaction/verify/:reference against Paystack's API. Compare the returned amount, currency, and status directly against the original transaction record stored in your database.
This verification must happen before you credit the user or deliver the product. If any value differs, reject the transaction immediately. Flag it for manual review in your admin panel. Check the currency code meticulously. Attackers frequently attempt to pay in a significantly cheaper currency. They initiate a payment for 1000 NGN. They manipulate the payload to process 1000 USD. If your code only validates the numeric integer, they steal the massive difference in exchange rates. You must check the currency string.
Handling floating point numbers introduces another critical risk vector. Representing currency as a float in JavaScript is a cardinal sin. Floating point math is imprecise. 0.1 + 0.2 === 0.30000000000000004. Always store and process currency in its smallest indivisible unit. Use Kobo. Use Cents. Process everything as integers. Failure to do this allows attackers to exploit rounding errors to siphon fractional amounts across millions of transactions.
This verification step is not optional. It forms the core backbone of how to secure payment apis in fintech startups. Without this step, an attacker who controls the client manipulates the amount parameter during initialisation. They forge a webhook claiming a higher amount was paid. The gateway's verification endpoint serves as your single source of truth. Trust absolutely nothing else.
Idempotency: preventing duplicate charges
Payment gateways retry webhooks aggressively. They retry whenever your server responds with anything other than a strict 2xx HTTP status code. Network glitches happen constantly. Load balancers timeout during traffic spikes. Deployment rollouts cause dropped connections. Your server will inevitably miss the initial delivery. The gateway will retry the payload.
Without strict idempotency controls, each retry processes the payment again. Your system credits wallets multiple times. It triggers third-party disbursements multiple times. It issues receipts multiple times. Attackers exploit this behavior intentionally. They capture a valid webhook payload. They replay it to your server hundreds of times concurrently. If your code lacks idempotency, they turn a single ₦1,000 payment into ₦1,000,000.
Use the transaction reference or the unique gateway event ID as an idempotency key. Before executing any business logic, check whether that specific reference already exists in a processed state. Store processed references in a database table enforced with a strict unique constraint. ALTER TABLE transactions ADD CONSTRAINT unique_reference UNIQUE (reference);. This pushes the idempotency guarantee down to the database level. It provides the strongest possible protection.
Alternatively, use a Redis set with a Time-To-Live (TTL) matching the gateway's maximum retry window. This window typically spans 24 to 72 hours. Respond with 200 OK to acknowledge receipt even when you skip duplicate processing. If you return an error code for a duplicate, the gateway keeps retrying. It pollutes your logs. It wastes CPU cycles. Idempotency guarantees that a single external payment event produces exactly one state change within your system.
BEGIN;
-- Lock the row to prevent concurrent webhook processing
SELECT status, amount, currency
FROM transactions
WHERE reference = 'tx_12345'
FOR UPDATE;
-- Application layer verifies gateway response against these locked values.
-- If the status is already 'successful', return 200 OK immediately and abort.
UPDATE transactions
SET status = 'successful'
WHERE reference = 'tx_12345';
COMMIT; Race conditions represent the most dangerous idempotency failure. If you read the database, execute logic, and then update the database, you create a Time-of-Check to Time-of-Use (TOCTOU) vulnerability. Two concurrent webhooks read the database simultaneously. Both see a pending status. Both execute the credit logic. Both update the row to successful. You just double-credited the user. You must use distributed locks in Redis or database row locks like SELECT ... FOR UPDATE in PostgreSQL. Ensure absolute atomicity.
Secret key management and rotation
Your Paystack or Flutterwave secret key holds equivalent destructive power to a production database password. If it leaks, the game ends immediately. An attacker uses it to initialise unauthorized transactions. They verify fake payments. They manipulate your account balance. They issue irreversible refunds to their own compromised cards. They destroy your business overnight.
Yet we routinely find secret keys committed directly to Git repositories. We find them stored in plain text .env files deployed to public web servers. We find them hardcoded in Android APKs and iOS IPA files. Stop doing this. If you want to know how to secure payment apis in fintech startups, start by protecting your cryptographic keys. Hardcoded secrets represent the lowest hanging fruit for threat actors. They run automated scanners that scrape GitHub continuously. They decompile mobile apps simply to run grep against the source strings.
Key rotation strategy
Store your secret keys in a dedicated, hardened secrets manager. Use AWS Secrets Manager, HashiCorp Vault, or Google Secret Manager. At a minimum, inject them securely as environment variables at runtime. Never write them to disk. Limit access to production secrets to a tiny fraction of your engineering team.
Rotate keys on a strict, defined schedule. Execute this rotation quarterly at a minimum. Rotate them instantly after any team member with production access departs the company. During a rotation event, you must support both the old and new keys for a brief overlap window. This prevents dropping in-flight webhooks. If you cut over instantly, any webhook signed with the old key during the deployment window will fail verification. Your system will reject legitimate payments. Your customer support queue will explode.
For teams utilising CI/CD pipelines, ensure secret keys get injected directly via the pipeline's native secure store. Never write them out to build artifacts. Never embed them inside container images. Use ephemeral, short-lived access tokens wherever possible to access the secrets manager. Restrict the blast radius of a compromised key fiercely.
Beware of source maps. When you compile frontend assets, Webpack or Vite often generates source maps. If you accidentally bundle an environment variable containing a secret key, the source map exposes it to the entire internet. Attackers look for .map files specifically to extract backend secrets that leaked into the frontend build process. Verify your build output locally before every production deployment. Ensure no environment variables leak into static files.
Are your payment integrations leaking secret keys or skipping server-side validation?
Book a Payment Security AuditPCI scope implications by integration type
Not all integrations carry the exact same PCI-DSS compliance burden. Understanding exactly where and how cardholder data flows through your infrastructure determines your compliance scope. Reducing scope reduces operating cost. It reduces audit complexity. It reduces technical risk. You must choose the right integration model.
Redirect / Hosted page
Examples include Paystack Popup, Flutterwave Standard, and Stripe Checkout. Card data never touches your servers. The user inputs their card on a secure form hosted entirely by the gateway. You face the lowest possible PCI scope. You only require an SAQ A questionnaire. This represents the strictly recommended integration for most Nigerian fintechs. It offloads the heaviest security burdens entirely to the gateway. Your risk surface shrinks drastically.
Inline / Embedded form
Examples include Paystack Inline and Stripe Elements. Card data gets tokenised directly in the browser before reaching your server. You inherit SAQ A-EP scope. Your page must be served over strict TLS. You must protect it aggressively against Cross-Site Scripting (XSS). If an attacker finds an XSS vulnerability on your checkout page, they inject malicious JavaScript. This script reads the raw keystrokes. It exfiltrates the card data before tokenisation even occurs. Implement a strict Content Security Policy (CSP). Implement Subresource Integrity (SRI) on the gateway's JavaScript SDK tags to prevent supply chain attacks.
Direct API / Server-to-server
This involves charging tokens or processing raw Primary Account Numbers (PAN) directly via backend API calls. You inherit full SAQ D scope. Your entire server environment falls into PCI scope. The auditor will examine your firewalls, your file integrity monitoring systems, your access logs, and your patch management process. Unless you possess a dedicated compliance team and a massive security budget, avoid this integration type entirely. It is a dangerous trap for early-stage fintechs. One server misconfiguration compromises all card data.
For a deeper, technical breakdown of PCI requirements specifically tailored for Nigerian fintechs, consult our PCI-DSS compliance guide.
Hardening checklist
You must implement these controls. Treat this as a mandatory checklist for every deployment. Never bypass a control for the sake of speed. Attackers exploit gaps within hours of deployment.
- Verify webhook signatures using HMAC with constant-time comparison before processing any business logic. Stop timing attacks entirely.
- Re-verify amounts server-side by directly calling the gateway's transaction verification endpoint. Do not trust the frontend payload under any circumstances.
- Validate currency codes strictly. Ensure the payment currency matches the database record precisely to prevent exchange rate manipulation.
- Process amounts as integers. Use smallest indivisible units like Kobo or Cents. Never use floating point numbers for financial calculations. Prevent rounding error exploits.
- Implement strict idempotency using transaction references to prevent duplicate processing and block replay attacks.
- Enforce database row locks during transaction updates using
SELECT FOR UPDATE. Prevent race conditions and double-crediting via concurrent webhooks. - Store secrets in a secrets manager. Never store them in code,
.envfiles on disk, or mobile app bundles. - Rotate secret keys quarterly. Rotate them instantly after personnel changes. Support overlap windows during rotation.
- Use the lowest-scope integration type. Default to hosted or redirect pages unless you possess a highly specific technical reason to increase your PCI scope.
- Restrict webhook endpoints. Limit access strictly to the gateway's published IP ranges. Implement this at the firewall or WAF level as a defense-in-depth layer.
- Log every single webhook event. Include its verification status, timestamp, and source IP. Retain this data for forensic analysis when an attack occurs.
- Enforce TLS 1.2 minimum. Reject older protocols. Use strong cipher suites for all incoming and outgoing connections.
- Implement strict rate limiting. Protect checkout endpoints to prevent card testing and BIN spinning attacks.
- Validate webhook timestamps. Reject payloads older than 5 minutes to mitigate long-term replay attacks.
- Implement Subresource Integrity (SRI). Add integrity hashes to gateway JavaScript SDKs to prevent supply chain compromise.
- Implement alerting for webhook failures. If signature verification fails more than 10 times in a minute, page the security team immediately. This indicates an active attack.
- Sanitise all gateway inputs. Even data from trusted payment providers can contain malicious payloads if the gateway itself suffers a compromise. Escape all outputs before rendering them to an admin panel.
- Conduct regular penetration testing. Automated scanners cannot find business logic flaws like idempotency failures or state machine bypasses. You need manual, adversarial testing to discover deep-rooted logic bugs.
If you are building a payment product in Nigeria, this represents foundational work. The fintech security checklist covers these exact controls and more in an actionable format. Hand it directly to your engineering team. Demand strict, uncompromising adherence.
Related reading
Blog: Payment gateway penetration testing · Webhook security for payment platforms · Rate limiting for payment APIs
Guides: PCI-DSS for Nigerian fintechs · Fintech security checklist · OWASP for fintech
Services: API security · Penetration testing