<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: uma victor</title>
    <description>The latest articles on Forem by uma victor (@umavictor6).</description>
    <link>https://forem.com/umavictor6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F297603%2F8557df28-c8db-44e0-91b2-7b3ffb2c0f11.png</url>
      <title>Forem: uma victor</title>
      <link>https://forem.com/umavictor6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/umavictor6"/>
    <language>en</language>
    <item>
      <title>Financial Integration in Africa: What Developers Need To Build For</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 10 Apr 2026 14:28:16 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/financial-integration-in-africa-what-developers-need-to-build-for-5b6a</link>
      <guid>https://forem.com/flutterwaveeng/financial-integration-in-africa-what-developers-need-to-build-for-5b6a</guid>
      <description>&lt;p&gt;You just shipped a payment integration in Nigeria. Nigeria Inter-Bank Payment (NIP) bank transfers work, your webhook handler is solid, and transactions are settling same-day. Then your product team says: "We're expanding to Kenya and Ghana next quarter."&lt;/p&gt;

&lt;p&gt;Suddenly, your NIP-specific code is useless in Nairobi, where M-Pesa processes the majority of consumer payments through a completely different API model. Your KYC flow, built around Nigeria's BVN, doesn't map to Kenya's IPRS or Ghana's mandatory Ghana Card verification. And your single-currency ledger now needs to handle three currencies with different FX volatility profiles.&lt;/p&gt;

&lt;p&gt;This is the structural challenge of building across African payment systems. The continent runs 36 instant payment systems across 31 countries, processes nearly &lt;a href="https://www.africanenda.org/uploads/files/siips2025/siips_2025_ExecutiveSummary_en.pdf" rel="noopener noreferrer"&gt;$2 trillion&lt;/a&gt; in instant payments annually, and operates 42 distinct currencies. There is no single "African payment rail." There are dozens of parallel financial ecosystems shaped by regulation, telecom infrastructure, banking penetration, and currency policy.&lt;/p&gt;

&lt;p&gt;This article gives you the concrete patterns to handle that complexity. By the end, you'll understand the five payment rail categories operating across the continent, know how to design a routing layer and adapter pattern that scales across markets, and have a clear picture of how compliance, settlement, and FX vary from country to country.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Payment Rail Categories You Need To Support
&lt;/h2&gt;

&lt;p&gt;Across Africa's major markets, payment infrastructure is not built on a single dominant rail. Most countries operate multiple parallel systems, and production-grade platforms must support several simultaneously.&lt;/p&gt;

&lt;p&gt;Five rail categories carry the bulk of transaction volume across the continent. Here's what each one demands from your system.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Instant Payment Systems
&lt;/h3&gt;

&lt;p&gt;Instant payment systems are interoperable rails that move funds between accounts in real time, whether those accounts sit in banks, mobile money wallets, or fintech platforms. Across Africa's largest markets, these systems carry the highest share of digital transaction volume, which means your integration will almost certainly touch one.&lt;/p&gt;

&lt;p&gt;Africa now has 36 live instant payment systems spanning 31 countries, up from 31 systems a year earlier. These processed 64 billion transactions worth &lt;a href="https://www.africanenda.org/uploads/files/siips2025/siips_2025_ExecutiveSummary_en.pdf" rel="noopener noreferrer"&gt;$1.98 trillion&lt;/a&gt; in 2024 (AfricaNenda SIIPS 2025). Nigeria Inter-Bank Payment System (NIP) alone handled over &lt;a href="https://nairametrics.com/2025/01/29/e-payment-transactions-in-nigeria-hit-all-time-high-of-n1-07-quadrillion-in-2024/" rel="noopener noreferrer"&gt;11.2 billion&lt;/a&gt; transactions in 2024, making it one of the largest real-time payment systems globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Considerations&lt;/strong&gt;&lt;br&gt;
Despite the "instant" label, confirmations are asynchronous. NIP uses deferred net settlement through the RTGS system. South Africa's PayShap enforces a 10-second maximum processing window but settles in batches through the SAMOS system multiple times per day.&lt;/p&gt;

&lt;p&gt;When you're building against these rails, three things need to be in your integration from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency keys on every request to prevent duplicate charges.&lt;/li&gt;
&lt;li&gt;Adaptive timeout logic, because response times vary from near-instant to 30 seconds depending on the bank.&lt;/li&gt;
&lt;li&gt;Provider-specific error handling, since error codes aren't standardized across banks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. Mobile Money
&lt;/h3&gt;

&lt;p&gt;Mobile money lets users store, send, and receive funds through their phone, no bank account needed. Transactions happen via USSD codes, SIM toolkit menus, or provider apps, and agent networks handle cash-in and cash-out. In East and parts of West Africa, mobile money is the primary financial interface, which means if you skip this rail, you're locking out the majority of your potential users.&lt;/p&gt;

&lt;p&gt;Sub-Saharan Africa has over 1.1 billion registered mobile money accounts, more than half of all registered accounts globally, processing $1.1 trillion annually (&lt;a href="https://www.gsma.com/sotir/" rel="noopener noreferrer"&gt;GSMA 2025&lt;/a&gt;). M-Pesa dominates Kenya with over 40 million monthly active users. MTN MoMo operates across 14 African countries with 69.5 million active users as of 2025. In East and parts of West Africa, mobile money is the primary financial interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Considerations&lt;/strong&gt;&lt;br&gt;
Your integration architecture needs to account for how these providers work from the start. Mobile money APIs follow a webhook/callback pattern, you register confirmation URLs, and the provider POSTs results asynchronously. M-Pesa's Daraja API uses OAuth 2.0 authentication and is designed to handle high-volume payments at scale. &lt;a href="https://momodeveloper.mtn.com/" rel="noopener noreferrer"&gt;MTN MoMo's Open API&lt;/a&gt; covers Collections, Disbursements, and Remittances, each with its own set of endpoints for initiating, confirming, and managing transactions. But the async nature creates three challenges you won't hit with instant bank rails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency is unpredictable as a confirmation might take two seconds or 45 seconds.&lt;/li&gt;
&lt;li&gt;Providers downtime is common and rarely announced in advance, so your system needs graceful degradation per provider.&lt;/li&gt;
&lt;li&gt;Batch settlement means your reconciliation logic has to tolerate a gap between "transaction confirmed" and "funds actually settled."&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. Card Payments
&lt;/h3&gt;

&lt;p&gt;In Nigeria, Interswitch's domestic scheme, Verve, dominates the card market with over 100 million cards issued across Africa and roughly 70% domestic market share, driven by Naira devaluation making FX-denominated card scheme fees uneconomical for most issuers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Considerations&lt;/strong&gt;&lt;br&gt;
When you're integrating card payments across these markets, your payment flow logic needs to handle scheme-level differences from the start. Pre-authorization versus capture flows work differently between Verve, Mastercard, and Visa, so you can't assume one flow fits all three.&lt;/p&gt;

&lt;p&gt;Chargeback response windows are tight and vary by card scheme and transaction type. Flutterwave's dispute resolution flow requires rapid merchant responses, which means your dispute handling pipeline needs to be automated, not manual. And 3D Secure enforcement can significantly reduce payment success rates, particularly on cards issued by banks with poor 3DS infrastructure. Your system needs to distinguish between soft declines (retry-eligible) and hard declines (terminal) to avoid throwing away transactions you could have recovered.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. USSD
&lt;/h3&gt;

&lt;p&gt;USSD still accounts for 63.5% of total mobile money transaction volume in Africa (&lt;a href="https://www.marketdataforecast.com/market-reports/africa-mobile-money-market" rel="noopener noreferrer"&gt;Market Data Forecast, 2024&lt;/a&gt;), rising to 89% in the WAEMU region. It works on any GSM phone without internet, making it the most reliable channel reaching populations where smartphone penetration stays extremely low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Considerations&lt;/strong&gt;&lt;br&gt;
If you're building a USSD payment flow, the constraints shape your UX from the first screen. Sessions typically timeout within 120 to 180 seconds, depending on the operator, with inactivity limits as short as 20 seconds on some networks. So every unnecessary step is a risk of losing the user. Messages are capped at around 160 characters, which means your prompts need to be short and unambiguous. Best practice is five steps or fewer per complete transaction. Dropped sessions are common, so your system needs fallback logic that can resume or safely cancel an incomplete transaction without double-charging.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Stablecoins
&lt;/h3&gt;

&lt;p&gt;Stablecoins now represent &lt;a href="https://www.chainalysis.com/blog/subsaharan-africa-crypto-adoption-2024/" rel="noopener noreferrer"&gt;43%&lt;/a&gt; of all crypto transaction volume in Sub-Saharan Africa. Nigeria alone processed nearly $22 billion in stablecoin transactions between July 2023 and June 2024. Flutterwave's &lt;a href="https://polygon.technology/blog/flutterwave-selects-polygon-as-its-default-blockchain-for-cross-border-payments" rel="noopener noreferrer"&gt;partnership&lt;/a&gt; with Polygon Labs (announced October 2025) makes Polygon the default blockchain for cross-border stablecoin payments across 30+ African markets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Considerations&lt;/strong&gt;&lt;br&gt;
Three architectural decisions need to be locked down before you write a line of stablecoin integration code. First, custody: Are you holding wallets on behalf of users, or are they connecting their own? This affects your licensing requirements in every market. Second, on-chain confirmation logic: You need to define how many block confirmations your system waits for before treating a transaction as final, and that threshold differs by chain. Third, reconciliation: Your internal ledger and the blockchain will drift, so you need a process that treats on-chain state as the source of truth and continuously reconciles against it.&lt;/p&gt;

&lt;p&gt;The regulatory picture also changes your implementation per market. South Africa's FSCA has been processing CASP license applications since 2023. Nigeria passed the &lt;a href="https://www.pwc.com/ng/en/publications/summary-of-key-changes-investments-securities-act-2025.html" rel="noopener noreferrer"&gt;ISA 2025&lt;/a&gt;, classifying digital assets as securities. &lt;a href="https://techcabal.com/2025/10/07/kenyas-crypto-bill-passes-third-reading/" rel="noopener noreferrer"&gt;Kenya's VASP Act&lt;/a&gt; was signed into law in October 2025. Ghana enacted &lt;a href="https://www.bog.gov.gh/virtual-assets/" rel="noopener noreferrer"&gt;VASP Act 1154&lt;/a&gt; in December 2025. Because these frameworks are still evolving, your stablecoin support should be modular, a feature flag you can toggle per country, not logic baked into your core payment flow.&lt;/p&gt;

&lt;p&gt;For a deeper walkthrough on how to add stablecoin settlement without rewriting your existing architecture, see &lt;a href="https://dev.to/flutterwaveeng/how-teams-integrate-stablecoin-rails-without-rewriting-their-platform-20fl"&gt;How Teams Integrate Stablecoin Rails Without Rewriting Their Platform&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architectural Patterns For Multi-Market Systems
&lt;/h2&gt;

&lt;p&gt;Knowing the different payment rails matters, but system design is what determines whether you rewrite your codebase every time you add a country. Two patterns do most of the heavy lifting: a routing layer that selects the right rail per market, and an adapter pattern that isolates each provider's requirements behind a shared interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Payment Routing Layer&lt;/strong&gt;&lt;br&gt;
Your routing layer detects the market, selects valid payment methods, and routes to the right provider. Here's a simplified routing example in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;preferredMethod&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RouteResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;routePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RouteResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RouteResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;NG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;bank_transfer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;banktransfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges?type=bank_transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Card charges require 3DES encryption;&lt;/span&gt;
      &lt;span class="na"&gt;ussd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ussd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges?type=ussd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;KE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;mpesa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mpesa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges?type=mpesa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;GH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;mobile_money&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mobile_money_ghana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges?type=mobile_money_ghana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flutterwave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v3/charges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countryRoutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;countryRoutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsupported market: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferredMethod&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;countryRoutes&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;countryRoutes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight here is that routing and orchestration are core layers, not extensions you bolt on later. When you add South Africa or Francophone West Africa, you add configuration, not code paths.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzrdufupa2akqc30tkwp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzrdufupa2akqc30tkwp.png" alt="payment routing flow" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: Adapter Pattern For Provider Abstraction&lt;/strong&gt;&lt;br&gt;
Each rail has its own authentication model, request format, and callback structure. An adapter pattern gives you a shared interface with market-specific implementations. Here's the structure in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChargeParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChargeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VerificationResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebhookResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MpesaAdapter&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChargeParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChargeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// M-Pesa charge via Flutterwave /v3/charges?type=mpesa&lt;/span&gt;
    &lt;span class="c1"&gt;// Flutterwave handles STK Push orchestration and callback routing&lt;/span&gt;
    &lt;span class="c1"&gt;// Settlement: batch, varies by transaction type&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NigeriaBankTransferAdapter&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChargeParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChargeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// NIP transfer via Flutterwave, immediate confirmation expected&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GhanaMobileMoneyAdapter&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;PaymentAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChargeParams&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChargeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Ghana mobile money charge via Flutterwave /v3/charges?type=mobile_money_ghana&lt;/span&gt;
    &lt;span class="c1"&gt;// Webhook-driven confirmation, Flutterwave handles provider routing&lt;/span&gt;
    &lt;span class="c1"&gt;// Settlement: batch, provider-dependent timing&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern prevents duplication across 36 different payment system APIs. When BCEAO's PI-SPI system goes live across eight WAEMU countries, you add one adapter. Your business logic stays untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance, Settlement, and Multi-Currency Operations
&lt;/h2&gt;

&lt;p&gt;Getting the rails and routing right is half the problem. The other half is everything that wraps around the transaction: identity verification that varies by country, settlement timelines that don't behave the same way across rails, and currency conversion logic that can destroy your margins if you treat it as an afterthought. Each of these needs to be a first-class concern in your architecture. Here's what that looks like in practice for identity verification, settlement timing, and currency conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KYC Is Different in Every Market&lt;/strong&gt;&lt;br&gt;
The identity verification your system requires changes completely between markets. Nigeria uses BVN and/or NIN, with a tiered system that governs what transaction limits each verification level unlocks. Kenya routes through the IPRS, and if you're integrating with Safaricom's ecosystem, identity checks tie into the &lt;a href="https://developer.safaricom.co.ke/apis" rel="noopener noreferrer"&gt;Daraja API&lt;/a&gt;. Ghana has mandated the Ghana Card (Ghanacard) as the primary form of identification, with the National Identification Authority phasing out other ID types for official transactions. South Africa operates under FICA, with CASP classification adding a separate layer for crypto service providers.&lt;/p&gt;

&lt;p&gt;The practical move is to build a compliance decision engine that routes to market-specific validation rules rather than hardcoding KYC flows into onboarding logic. Each market's identity provider becomes an adapter, just like your payment rails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settlement Timing Is Not Uniform&lt;/strong&gt;&lt;br&gt;
NIP settles through deferred net, but confirmation is near-instant. M-Pesa wallet-to-wallet is instant, but B2C disbursements may be delayed. Cards settle T+1 domestically, with chargeback exposure lasting months. Stablecoins confirm on-chain in seconds, but full on/off-ramp flows take longer.&lt;/p&gt;

&lt;p&gt;Your internal ledger must model settlement states explicitly: pending, confirmed, final, and reversible. If your product logic assumes instant finality everywhere, it will break the moment you cross a market boundary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5ggxw6dip1r8wb4grcl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5ggxw6dip1r8wb4grcl.png" alt="Settlement timing by payment rail" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Africa Runs On 42 Currencies&lt;/strong&gt;&lt;br&gt;
Africa operates forty-two distinct currencies across 54 countries, with two CFA franc zones (14 countries) and the Rand Monetary Area providing the only real shared-currency pockets. Most African currency pairs must route through USD or EUR as intermediaries, adding conversion layers and cost. Sending money to Sub-Saharan Africa remains the most expensive corridor globally, with average costs at 8.78% as of Q1 2025 according to the &lt;a href="https://remittanceprices.worldbank.org/sites/default/files/rpw_main_report_and_annex_q125_1_0.pdf" rel="noopener noreferrer"&gt;World Bank's Remittance Prices Worldwide database&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Build FX quoting, rate locking, and conversion timing into your core domain model from day one. This is not a feature to add later. Nigeria's Naira alone fell from roughly &lt;a href="https://tradingeconomics.com/nigeria/currency" rel="noopener noreferrer"&gt;₦460 to over ₦1,700&lt;/a&gt; per US dollar in the months following the June 2023 FX unification. The rate has since partially recovered but remains volatile, trading around ₦1,384 as of early 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principles For Multi-Market Africa Payment Systems
&lt;/h2&gt;

&lt;p&gt;Six principles that hold up no matter which market you add next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for rail plurality, not rail preference:&lt;/strong&gt; In Nigeria, instant bank transfers dominate. In Kenya, it's mobile money. In South Africa, cards. Your routing and orchestration layers must be core infrastructure, not add-ons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat compliance as programmable infrastructure:&lt;/strong&gt; BVN is not IPRS, is not Ghana Card, is not FICA. Build a rule-based compliance engine with extensible identity provider adapters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstract settlement timing from product logic:&lt;/strong&gt; Model pending, confirmed, final, and reversible states. Never assume uniform finality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep rail-specific logic isolated:&lt;/strong&gt; Market logic belongs in adapters, routing rules, and compliance modules, not in business logic. This is the difference between adding a country in a week versus rewriting your payment system every six months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan for uneven infrastructure quality:&lt;/strong&gt; Payment success rates vary widely across African markets, depending on method and provider. Build circuit breakers per market and provider. Use exponential backoff for retries. Always re-verify transactions through the verification endpoint rather than trusting a single webhook delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate your ledger from provider state:&lt;/strong&gt; Your internal ledger must be canonical and independent of any provider's API. Reconcile, don't depend. A single webhook is never the final truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Once, Scaling Across Markets
&lt;/h2&gt;

&lt;p&gt;Africa's payment infrastructure keeps adding layers. &lt;a href="https://dev.to/flutterwaveeng/what-papss-means-for-developers-building-cross-border-payments-in-africa-10mj"&gt;PAPSS&lt;/a&gt; now connects 19 countries for cross-border clearing, half of all instant payment systems support cross-domain interoperability (&lt;a href="https://www.africanenda.org/uploads/files/siips2025/siips_2025_ExecutiveSummary_en.pdf" rel="noopener noreferrer"&gt;AfricaNenda SIIPS 2025&lt;/a&gt;), and mobile money providers are pushing into cards, savings, and cross-border transfers. Stablecoins are emerging as corridor-specific rails for B2B and remittance flows. For your system, that means the surface area you need to cover is only growing.&lt;/p&gt;

&lt;p&gt;The next time your product team says, "We're adding Kenya next quarter," your answer isn't a rewrite. It's a new adapter, a routing rule, and a compliance config.&lt;/p&gt;

&lt;p&gt;Platforms like Flutterwave already abstract much of this complexity. A single API call handles M-Pesa in Kenya, bank transfers in Nigeria, and mobile money in Ghana, without separate integrations per rail. Whether you build direct integrations or work through a unified API, the architectural discipline is the same: route dynamically, abstract aggressively, isolate market logic, and never assume any two countries work the same way.&lt;/p&gt;

&lt;p&gt;Africa payment systems reward builders who treat fragmentation as a design constraint, not a problem to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Deeper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For a walkthrough on adding stablecoin settlement without rewriting your existing stack, read &lt;a href="https://dev.to/flutterwaveeng/how-teams-integrate-stablecoin-rails-without-rewriting-their-platform-20fl"&gt;How Teams Integrate Stablecoin Rails Without Rewriting Their Platform&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;To build cross-border payments across Nigeria, Kenya, and Ghana in a single codebase, see &lt;a href="https://dev.to/flutterwaveeng/setting-up-cross-border-payments-in-a-single-codebase-la8"&gt;Setting Up Cross-Border Payments in a Single Codebase&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Explore the &lt;a href="https://developer.flutterwave.com" rel="noopener noreferrer"&gt;Flutterwave developer documentation&lt;/a&gt; to see how routing, settlement, and multi-currency work across 34+ countries.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>integration</category>
      <category>stablecoin</category>
      <category>fintech</category>
      <category>development</category>
    </item>
    <item>
      <title>On-Chain vs. Off-Chain Payments: What's Best for Your System</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 27 Mar 2026 09:27:05 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/on-chain-vs-off-chain-payments-whats-best-for-your-system-3dja</link>
      <guid>https://forem.com/flutterwaveeng/on-chain-vs-off-chain-payments-whats-best-for-your-system-3dja</guid>
      <description>&lt;p&gt;You're building a payment platform that needs to handle cross-border settlements for merchants in Lagos and Nairobi. A $10,000 B2B payment through traditional correspondent banking costs $25–$100+ in direct fees, plus FX markups that can add another 2–5% on African corridors. It takes three to five business days. Send that same payment on-chain as USDC, and it costs under $0.10 and confirms in seconds.&lt;/p&gt;

&lt;p&gt;But here's the catch: That Layer-2 network processes roughly 1,000 transactions per second in practice. A single tuned PostgreSQL instance handles 70,000+. For high-volume payment processing, you can't put every transaction on-chain. Do you go on-chain, off-chain, or both?&lt;/p&gt;

&lt;p&gt;If you've been wrestling with this question, you're asking the wrong one. The on-chain vs. off-chain debate misses the point. The better question is: Which parts of your payment flow benefit from blockchain finality, and which parts need database speed?&lt;/p&gt;

&lt;p&gt;This guide is for engineering leads and payment architects evaluating whether to add blockchain settlement to their payment stack. It covers cost and performance tradeoffs, when blockchain settlement adds value, hybrid architecture patterns, and a decision framework for your specific payment flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the On-Chain vs. Off-Chain Debate Misses the Point
&lt;/h2&gt;

&lt;p&gt;Most content about on-chain vs. off-chain transactions falls into two camps. Crypto-native content claims everything should be on-chain for transparency and decentralization. Traditional fintech content dismisses blockchain entirely because databases are faster and cheaper. Both positions miss the point.&lt;/p&gt;

&lt;p&gt;Here's what they get wrong: Traditional payments already use hybrid architecture. When you swipe a card at a store, Visa authorizes the transaction in real-time, in under one second. But actual settlement happens in daily batch cycles where net obligations between issuing and acquiring banks are calculated and transferred. Millions of off-chain transactions roll up into periodic settlements.&lt;/p&gt;

&lt;p&gt;The practical question isn't on-chain vs. off-chain. It's which payment flows need public finality, counterparty trust, or cross-border settlement, and which need speed, privacy, or cost optimization.&lt;/p&gt;

&lt;p&gt;You have three architecture patterns to choose from. Pure off-chain uses traditional database-based processing. Pure on-chain puts every transaction on the blockchain. Hybrid combines off-chain processing with periodic on-chain settlement. Most production payment systems that interact with blockchain use a hybrid approach.&lt;/p&gt;

&lt;p&gt;If you're building &lt;a href="https://dev.to/flutterwaveeng/setting-up-cross-border-payments-in-a-single-codebase-la8"&gt;cross-border payment infrastructure&lt;/a&gt;, understanding this distinction matters for every technical decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding On-Chain Payments
&lt;/h2&gt;

&lt;p&gt;When a payment happens on-chain, the transaction gets recorded on a public blockchain like Ethereum or Polygon. Network validators verify it. Once confirmed, the transaction becomes immutable.&lt;/p&gt;

&lt;p&gt;On-chain transactions have four characteristics that matter for payment systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finality:&lt;/strong&gt; After confirmation, the transaction can't be reversed. Ethereum mainnet achieves &lt;a href="https://ethereum.org/developers/docs/consensus-mechanisms/pos/" rel="noopener noreferrer"&gt;finality in ~13–15 minutes&lt;/a&gt; (2 epochs under Proof-of-Stake). Layer-2 networks like Base or Arbitrum provide sequencer confirmation (block inclusion) in 2–5 seconds, though true finality depends on Ethereum L1 (~15+ minutes) and withdrawals to L1 carry a 7-day challenge window for optimistic rollups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transparency:&lt;/strong&gt; Anyone can verify that a transaction occurred. This is useful for audits, dispute resolution, and regulatory reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counterparty trust elimination:&lt;/strong&gt; The blockchain enforces settlement. No intermediary is required, and no party needs to trust the other's internal ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Every on-chain transaction incurs gas fees. Ethereum mainnet typically runs $0.30–$0.50 for a token transfer during low-traffic periods, though fees are volatile and can spike significantly. Layer-2 networks charge $0.003–$0.09 for token transfers after the &lt;a href="https://www.coindesk.com/tech/2024/03/12/ethereum-blockchain-counts-down-to-dencun-upgrade-set-to-reduce-fees" rel="noopener noreferrer"&gt;Dencun upgrade&lt;/a&gt; slashed data-posting costs by 90 percent or more.&lt;/p&gt;

&lt;p&gt;On-chain settlement makes sense for &lt;a href="https://dev.to/flutterwaveeng/b2b-payments-best-practices-to-secure-payment-processing-241m"&gt;high-value B2B transactions&lt;/a&gt; where finality justifies the cost, cross-border settlements where you want to eliminate correspondent banks, and situations where counterparties don't trust each other's internal systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Off-Chain Payments
&lt;/h2&gt;

&lt;p&gt;Off-chain payments record transactions in application databases, APIs, or internal ledgers without involving the blockchain for each payment. This is how most payment processing works today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Database write latency runs 10–50 milliseconds. Users see instant confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Per-transaction cost is negligible: just server and database expenses. No gas fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; Transaction details stay private to system participants. No public visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalability:&lt;/strong&gt; Hundreds of thousands of transactions per second are achievable with proper infrastructure. You're limited by database and server capacity, not blockchain throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust requirement:&lt;/strong&gt; Users must trust the platform operating the ledger. If Flutterwave processes your payment off-chain, you're trusting Flutterwave's systems.&lt;/p&gt;

&lt;p&gt;Off-chain gives you speed and cost efficiency. On-chain gives you trustless finality. Most payment platforms combine both: off-chain for user-facing transactions where speed matters, with periodic on-chain settlement for finality.&lt;/p&gt;

&lt;p&gt;So how do these tradeoffs look in practice?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost and Performance Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Choosing between on-chain and off-chain comes down to three variables: cost per transaction, settlement speed, and throughput capacity. The tables below compare these across internal ledgers, traditional rails, and blockchain networks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Transaction costs and network throughput metrics are estimates as of Q1 2026 and are subject to network volatility and corridor-specific pricing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Transaction Costs Across Different Approaches&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfvtpnyatwo7ob0q5p2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfvtpnyatwo7ob0q5p2e.png" alt="Transaction cost across different approaches" width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; Card network fees based on &lt;a href="https://usa.visa.com/dam/VCOM/download/merchants/visa-usa-interchange-reimbursement-fees.pdf" rel="noopener noreferrer"&gt;Visa&lt;/a&gt; and &lt;a href="https://www.mastercard.com/us/en/business/support/merchant-interchange-rates.html" rel="noopener noreferrer"&gt;Mastercard&lt;/a&gt; published interchange fee schedules (updated semiannually). SWIFT costs per &lt;a href="https://remittanceprices.worldbank.org/" rel="noopener noreferrer"&gt;World Bank Remittance Prices Worldwide&lt;/a&gt; and &lt;a href="https://www.bankrate.com/banking/wire-transfer-fees/" rel="noopener noreferrer"&gt;Bankrate (2026)&lt;/a&gt;. Mobile money fees per &lt;a href="https://www.gsma.com/sotir/wp-content/uploads/2025/04/The-State-of-the-Industry-Report-2025_English.pdf" rel="noopener noreferrer"&gt;GSMA State of the Industry Report on Mobile Money 2025&lt;/a&gt; (domestic P2P). On-chain gas fees based on &lt;a href="https://etherscan.io/gastracker" rel="noopener noreferrer"&gt;Etherscan&lt;/a&gt;, &lt;a href="https://polygonscan.com/gastracker" rel="noopener noreferrer"&gt;PolygonScan&lt;/a&gt;, &lt;a href="https://arbiscan.io/chart/gasprice" rel="noopener noreferrer"&gt;Arbiscan&lt;/a&gt;, and &lt;a href="https://l2fees.info/" rel="noopener noreferrer"&gt;L2fees.info&lt;/a&gt; gas trackers (Q1 2026 snapshots). Internal ledger cost derived from &lt;a href="https://aws.amazon.com/rds/pricing/" rel="noopener noreferrer"&gt;AWS RDS&lt;/a&gt; and &lt;a href="https://cloud.google.com/sql/pricing" rel="noopener noreferrer"&gt;GCP Cloud SQL&lt;/a&gt; per-write pricing estimates.&lt;/p&gt;

&lt;p&gt;Note: Internal ledger costs exclude compliance, fraud detection, and reconciliation overhead. Traditional rails include these costs. On-chain costs are gas fees for token transfers (e.g., USDC, USDT) and don't include custody or bridging infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Settlement Speed&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Confirmation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Settlement finality&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Off-chain (internal ledger)&lt;/td&gt;
&lt;td&gt;&amp;lt;100ms&lt;/td&gt;
&lt;td&gt;T+1 to T+3 (depends on payout rails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layer-2 (sequencer confirmation)&lt;/td&gt;
&lt;td&gt;2–5 seconds&lt;/td&gt;
&lt;td&gt;~15+ min (L1 finality)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum mainnet&lt;/td&gt;
&lt;td&gt;~12 seconds&lt;/td&gt;
&lt;td&gt;~13–15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polygon PoS&lt;/td&gt;
&lt;td&gt;~2 seconds&lt;/td&gt;
&lt;td&gt;~30 min (checkpoint to L1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traditional cross-border (SWIFT)&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;1–5 business days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: "Confirmation" is when the user sees success. "Settlement finality" is when funds are irreversibly transferred.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Transactions per second&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL (tuned)&lt;/td&gt;
&lt;td&gt;70,000+ (simple operations)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;~1,000 effective (~3,500 including validator votes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polygon PoS&lt;/td&gt;
&lt;td&gt;700–1,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base&lt;/td&gt;
&lt;td&gt;~1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum mainnet&lt;/td&gt;
&lt;td&gt;15–20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: These aren't equivalent workloads. A PostgreSQL write inserts a row. An Ethereum transaction involves consensus across thousands of nodes, cryptographic verification, and global state changes. The numbers show raw capacity, not like-for-like comparison.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The Visa TPS Myth&lt;/strong&gt;&lt;br&gt;
Visa's commonly cited "65,000 TPS" is a theoretical peak capacity from 2017. Their actual average throughput, calculated from &lt;a href="https://s1.q4cdn.com/050606653/files/doc_financials/2024/q4/Q4-2024-Earnings-Release_vF.pdf" rel="noopener noreferrer"&gt;233.8 billion transactions in fiscal year 2024&lt;/a&gt;, runs approximately 7,400–9,300 TPS. Still higher than any blockchain, but not the gap marketing materials suggest.&lt;br&gt;
But even before you hit throughput limits, the economics break down. Consider an e-commerce platform processing 50,000 transactions daily (barely 1 TPS):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure on-chain (mainnet): $15,000–$25,000 daily in fees. Uneconomical.&lt;/li&gt;
&lt;li&gt;Pure on-chain (Layer-2): $150–$4,500 daily in fees. Expensive but possible.&lt;/li&gt;
&lt;li&gt;Off-chain with daily batched settlement: $0.30–$0.50 daily per merchant (rolling up thousands of user payments into a single end-of-day settlement transaction). Highly economical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The math points clearly toward hybrid architecture for high-volume use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  When On-Chain Settlement Makes Sense
&lt;/h2&gt;

&lt;p&gt;On-chain settlement adds value in specific scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-border B2B settlements:&lt;/strong&gt; Traditional correspondent banking takes 1–5 days and costs $25–$100+ in fees, plus FX markups of 2–5 percent above mid-market rates. Stablecoin settlement on a Layer-2 network provides finality in seconds for under $0.10. For a remittance company settling with a partner bank in Nairobi, the economics favor on-chain settlement.&lt;/p&gt;

&lt;p&gt;The infrastructure for &lt;a href="https://dev.to/flutterwaveeng/setting-up-cross-border-payments-in-a-single-codebase-la8"&gt;cross-border&lt;/a&gt; stablecoin payouts is expanding fast. USDC has processed over $25 trillion in cumulative on-chain transaction volume since 2018, with Q3 2025 alone accounting for roughly $9.6 trillion. PayPal launched PYUSD stablecoin in 2023, Visa has run stablecoin settlement pilots since 2021, and Mastercard announced stablecoin payment capabilities in 2024.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-value transactions requiring immediate finality:&lt;/strong&gt; Off-chain ledgers can theoretically be reversed by the platform operator. On-chain settlement means the transaction becomes irreversible after confirmation. For property purchases, large B2B invoices above $100,000, or transactions where finality certainty justifies the cost, on-chain provides assurance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counterparty distrust scenarios:&lt;/strong&gt; When international partners don't trust each other's internal ledgers, a neutral blockchain provides shared truth. Neither party can alter the record unilaterally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulatory transparency requirements:&lt;/strong&gt; Some jurisdictions require auditable transaction records. Public blockchains provide an immutable audit trail for regulatory reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulatory Considerations&lt;/strong&gt;&lt;br&gt;
Compliance requirements vary by jurisdiction, but regulators generally focus on outcomes (transparency, auditability, consumer protection), not implementation specifics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.esma.europa.eu/esmas-activities/digital-finance-and-innovation/markets-crypto-assets-regulation-mica" rel="noopener noreferrer"&gt;&lt;strong&gt;MiCA&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;(EU):&lt;/strong&gt; Fully enforced since December 2024, MiCA classifies stablecoins as E-Money Tokens or Asset-Referenced Tokens. It requires 1:1 liquid reserves, segregated custody, and regular audits. But it doesn't mandate that every payment transaction be on-chain. Off-chain processing is allowed as long as issuers can prove reserves. The practical impact: Several major EU exchanges restricted or delisted USDT for retail users under MiCA compliance requirements, while USDC remains available as the first MiCA-compliant global stablecoin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ng.andersen.com/the-investment-and-securities-act-2025-a-new-era-for-digital-assets-and-financial-disclosure/" rel="noopener noreferrer"&gt;&lt;strong&gt;Nigeria's ISA 2025&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Signed in March 2025, this law expands the definition of securities to include digital assets, bringing them under SEC oversight. VASPs must meet capital adequacy requirements and comply with &lt;a href="https://www.fatf-gafi.org/en/topics/virtual-assets.html" rel="noopener noreferrer"&gt;Travel Rule requirements per FATF guidance&lt;/a&gt;. On-chain isn’t mandated, but audit trails are.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://njagaadvocates.com/the-virtual-asset-service-providers-vasp-act-2025-is-now-law-a-new-era-for-crypto-digital-finance-in-kenya/" rel="noopener noreferrer"&gt;&lt;strong&gt;Kenya's VASP Act 2025&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Signed in October 2025, it introduces a licensing framework with the Central Bank of Kenya overseeing stablecoin issuers. Detailed regulations are still pending.&lt;/p&gt;

&lt;p&gt;The pattern across jurisdictions: Regulators care about auditability and transparency, which can be achieved through off-chain audit logs. On-chain settlement is one way to achieve compliance, not the only way.&lt;/p&gt;

&lt;p&gt;On-chain works for specific use cases. For many payment flows, off-chain is the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Off-Chain Is Superior
&lt;/h2&gt;

&lt;p&gt;Off-chain processing works better in several scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-frequency, low-value transactions:&lt;/strong&gt; When gas fees exceed a meaningful percentage of transaction value, off-chain is the only practical choice. Mobile money transfers of $5–$50, in-app purchases, subscription payments — these belong off-chain with batch settlement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy requirements:&lt;/strong&gt; On-chain transactions are visible to anyone with blockchain access. For payroll, healthcare payments, or any scenario where transaction privacy matters, off-chain keeps details internal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant user experience:&lt;/strong&gt; Even fast blockchains add seconds of latency that users notice at checkout. For e-commerce, point-of-sale, and peer-to-peer transfers, off-chain processing gives instant feedback. Settle on-chain later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex transaction logic:&lt;/strong&gt; Multi-party splits, conditional payments, subscription billing with trials — these take more effort to implement and maintain in smart contracts than in application code. Off-chain gives you the full power of your application code.&lt;/p&gt;

&lt;p&gt;Most production systems don't choose one or the other. They combine both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;Most production systems use a hybrid pattern. The blockchain handles the middle mile (settlement between counterparties) while traditional systems handle customer-facing processing, identity verification, and local currency conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Off-chain transactions with batch settlement:&lt;/strong&gt; Process transactions in your internal ledger instantly, with no blockchain fees. At end of day (or week), aggregate and settle the net balance on-chain with a single transaction.&lt;/p&gt;

&lt;p&gt;Picture 10,000 daily payments totaling $500,000. Settling each one individually on a Layer-2 network would cost $30–$900 in gas fees. Batch them into a single end-of-day settlement, and you pay a few cents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: Threshold-based routing:&lt;/strong&gt; Route transactions by value. Payments under a threshold (say, $1,000) batch-settle off-chain daily, while larger transactions settle immediately on-chain for the finality assurance that justifies the gas cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: On-chain settlement with off-chain metadata:&lt;/strong&gt; Record the value transfer on-chain. Sender, receiver, and amount are publicly verifiable. Store everything else (customer info, order details, line items) in your database. You get blockchain proof of payment without exposing sensitive data or bloating transaction costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 4: Layer-2 with mainnet checkpoints:&lt;/strong&gt; Process transactions on a Layer-2 for speed and low cost. The Layer-2 periodically posts batch proofs to Ethereum mainnet for ultimate security. You get Layer-2 economics with mainnet guarantees.&lt;/p&gt;

&lt;p&gt;For most payment platforms, Pattern 1 (batch settlement) or Pattern 2 (threshold routing) makes the most sense. They're simpler to implement and provide the best balance of cost, speed, and finality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Framework: Questions to Determine Your Architecture
&lt;/h2&gt;

&lt;p&gt;Work through these questions with your team to determine your approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What's Your Transaction Volume and Value Distribution?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 1,000 transactions daily, mostly under $100: Off-chain is sufficient. Batch-settle daily.&lt;/li&gt;
&lt;li&gt;1,000–50,000 transactions daily with mixed values: Hybrid. Off-chain for smaller amounts, on-chain for larger ones.&lt;/li&gt;
&lt;li&gt;Over 50,000 transactions daily, mostly under $100: Pure off-chain with daily batch settlement.&lt;/li&gt;
&lt;li&gt;High-value transactions over $10,000, regardless of volume: Benefit from on-chain settlement for faster finality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Do You Need Immediate Settlement Finality?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes, within seconds: On-chain via Layer-2.&lt;/li&gt;
&lt;li&gt;Yes, but hours are acceptable: Off-chain with hourly batch settlement.&lt;/li&gt;
&lt;li&gt;T+1 or T+3 acceptable: Off-chain with daily or weekly settlement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Are You Processing Cross-Border Payments?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes, and settlement with international partners is critical: On-chain eliminates correspondent banks.&lt;/li&gt;
&lt;li&gt;Yes, but only for accounting: Off-chain with periodic reconciliation.&lt;/li&gt;
&lt;li&gt;Domestic only: Off-chain likely sufficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. What's Your Privacy Requirement?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High (payroll, healthcare): Off-chain keeps details private.&lt;/li&gt;
&lt;li&gt;Medium (e-commerce): Hybrid. On-chain value transfer, off-chain metadata.&lt;/li&gt;
&lt;li&gt;Low (public transparency valuable): On-chain provides the audit trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Do Users Expect Instant Confirmation?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes, under 100 milliseconds required: Off-chain for UX, settle later.&lt;/li&gt;
&lt;li&gt;2–5 seconds acceptable: Layer-2 on-chain is feasible.&lt;/li&gt;
&lt;li&gt;Asynchronous (B2B invoices): Settlement timing is less critical; optimize for cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. How Much Blockchain Complexity Can Your Team Handle?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low (small team, product focus): Off-chain is simpler. Avoid blockchain complexity until you need it.&lt;/li&gt;
&lt;li&gt;Medium: Consider Layer-2 for the best balance.&lt;/li&gt;
&lt;li&gt;High (blockchain engineers on staff): Sophisticated hybrid patterns are possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;7. What's Your Regulatory Exposure?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy transparency requirements: On-chain provides built-in auditability that can simplify compliance.&lt;/li&gt;
&lt;li&gt;Regulated but implementation-agnostic: Off-chain with strong audit trails is sufficient.&lt;/li&gt;
&lt;li&gt;Minimal regulation: Optimize for cost and performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Flutterwave Can Help With Hybrid Architecture
&lt;/h2&gt;

&lt;p&gt;Building payment infrastructure that spans traditional rails and blockchain settlement is complex. It requires understanding both worlds and designing systems that move smoothly between them. Flutterwave processes billions of dollars annually across traditional payment rails (cards, bank transfers, mobile money) with coverage across 30+ African countries. That operational experience informs how we think about blockchain integration: pragmatically, not ideologically.&lt;/p&gt;

&lt;p&gt;Blockchain settlement adds value for cross-border finality and cost reduction in specific corridors. Traditional infrastructure performs better for high-frequency domestic transactions.&lt;/p&gt;

&lt;p&gt;For developers, this means APIs that abstract complexity. You shouldn't need to understand gas fees or bridge mechanics to process payments. Flutterwave handles the infrastructure decisions so you can focus on your product.&lt;/p&gt;

&lt;p&gt;Each payment system should go off-chain, on-chain, or hybrid according to the team's specific needs. The decision framework in this guide helps you evaluate your options. When you're ready to implement, &lt;a href="https://developer.flutterwave.com/" rel="noopener noreferrer"&gt;explore Flutterwave's payment APIs&lt;/a&gt; for traditional payment capabilities and &lt;a href="https://flutterwave.com/us/stablerails" rel="noopener noreferrer"&gt;StableRails&lt;/a&gt; for stablecoin settlement.&lt;/p&gt;

</description>
      <category>stablecoin</category>
      <category>payments</category>
      <category>flutterwave</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How Teams Integrate Stablecoin Rails Without Rewriting Their Platform</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:41:56 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/how-teams-integrate-stablecoin-rails-without-rewriting-their-platform-20fl</link>
      <guid>https://forem.com/flutterwaveeng/how-teams-integrate-stablecoin-rails-without-rewriting-their-platform-20fl</guid>
      <description>&lt;p&gt;Adding a new settlement rail to an existing payment system raises the same question every time, whether you are an early-stage team or a platform processing millions of cross border transactions: How much of our current architecture do we need to change? With stablecoins, that question feels heavier because the underlying technology is different. But in practice, integrating stablecoin settlement usually means adding one more settlement path, not rebuilding what you already have.&lt;/p&gt;

&lt;p&gt;If your system already handles pending states and delayed settlement, you are closer than you think.&lt;/p&gt;

&lt;p&gt;This blog post covers where stablecoins fit in a typical payment stack, how to add them as a parallel settlement path, how to handle settlement finality safely, and how to roll the whole thing out without putting production at risk.&lt;/p&gt;

&lt;p&gt;But before getting into architecture, there is a mental shift that changes how you approach the rest of the integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Shift Teams Get Wrong
&lt;/h2&gt;

&lt;p&gt;The first mistake most engineering teams make is treating &lt;a href="https://dev.to/flutterwaveeng/what-are-stablecoins-understand-how-they-work-3kg9"&gt;stablecoins&lt;/a&gt; like a new payment product. Stablecoins are settlement rails, digital assets pegged to fiat currency that move value between parties. That distinction changes everything about how you approach the integration.&lt;/p&gt;

&lt;p&gt;Think about how your system handles cards versus bank transfers today. A customer pays through the same checkout, creates the same payment intent, triggers the same authorization logic. The only difference is what happens after authorization: One payment settles through a card network over T+2, and the other settles through ACH over T+1. The checkout stayed the same. The reporting stayed the same. Only the settlement path diverged.&lt;/p&gt;

&lt;p&gt;Stablecoins work the same way. They are one more way to move money from point A to point B after a payment is initiated. Integration happens at the settlement layer. Your payment initiation and authorization logic stay untouched.&lt;/p&gt;

&lt;p&gt;Think of it like adding a new courier service to an e-commerce platform. Your order system, inventory management, and checkout stay exactly the same. You just add a new fulfillment adapter that ships packages through a different carrier.&lt;/p&gt;

&lt;p&gt;This is why the biggest payment companies are adding stablecoin rails without overhauling their platforms. Stripe treats stablecoins as just another &lt;a href="https://docs.stripe.com/payments/accept-stablecoin-payments" rel="noopener noreferrer"&gt;payment method&lt;/a&gt; inside its existing payment methods. Visa runs &lt;a href="https://usa.visa.com/about-visa/newsroom/press-releases.releaseId.21951.html" rel="noopener noreferrer"&gt;USDC settlement&lt;/a&gt; on &lt;a href="https://solana.com/" rel="noopener noreferrer"&gt;Solana&lt;/a&gt; between issuers and acquirers while the consumer card experience stays completely unchanged. Mastercard partnered &lt;a href="https://www.mastercard.com/news/ap/en/newsroom/press-releases/en/2025/mastercard-and-thunes-bring-stablecoin-payouts-to-the-mainstream/" rel="noopener noreferrer"&gt;with Thunes&lt;/a&gt; to route stablecoin payouts through the same Mastercard Move network that handles fiat. The pattern here is stablecoins slot in at the settlement layer while everything above it stays untouched.&lt;/p&gt;

&lt;p&gt;Once you see stablecoins as a settlement path rather than a product category, the integration points become obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Stablecoin Infrastructure Fits Into a Typical Payment Stack
&lt;/h2&gt;

&lt;p&gt;Most payment systems follow a similar flow regardless of the rails underneath. A payment intent is created, authorization checks run, settlement executes through the chosen rail, and a confirmation event closes the loop. Stablecoins primarily integrate at the settlement layer, without changing payment intent or authorization logic.&lt;/p&gt;

&lt;p&gt;The flow before and after adding stablecoin rails:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi247s80430xh82y84lrp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi247s80430xh82y84lrp.png" alt="before vs after payment flow" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Architecturally, nothing changes upstream of the settlement router. Your payment intent creation, fraud detection, KYC checks, and risk scoring are all settlement-agnostic and do not care whether the money ultimately moves through SWIFT, ACH, card networks, or a blockchain. The only new component is a settlement adapter that speaks the stablecoin protocol.&lt;/p&gt;

&lt;p&gt;Your existing webhook and callback infrastructure already handles the patterns stablecoins need. If your system can process an asynchronous "payment completed" event from a bank, it can process one from a blockchain confirmation service. The event shape is nearly identical: a transaction identifier, a status, an amount, a timestamp.&lt;/p&gt;

&lt;p&gt;For teams running payment &lt;a href="https://developer.flutterwave.com/docs/payment-orchestrator-flow" rel="noopener noreferrer"&gt;orchestration&lt;/a&gt; layers, the orchestration layer already abstracts multiple payment service providers behind a unified API. Adding a stablecoin settlement adapter works the same way as adding a new PSP. Your routing engine evaluates the best rail per transaction based on corridor and cost, and the stablecoin adapter becomes one more option in that evaluation.&lt;/p&gt;

&lt;p&gt;What about the differences? There are a few, and they matter. Stablecoins are push-based (the sender initiates the transfer), while cards are pull-based (the merchant requests funds). Stablecoins confirm asynchronously in seconds to minutes, while card authorizations return synchronously in milliseconds. And stablecoins do not have native chargeback mechanisms, so refund and dispute handling lives in your application layer.&lt;/p&gt;

&lt;p&gt;But none of these differences require architectural changes to your payment system. Your system already handles asynchronous confirmation (ACH does the same thing). Your system already handles rails without chargebacks (wire transfers work this way).&lt;/p&gt;

&lt;p&gt;The infrastructure you already have covers about 80% of what stablecoin integration requires. The remaining 20% is the settlement adapter itself and confirmation tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Stablecoins as a Parallel Payment Rail
&lt;/h2&gt;

&lt;p&gt;Stablecoins should plug in after payment initiation. Treat them as an alternative settlement backend that produces finality events, exactly like your other rails do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identify the Boundary Where Settlement Diverges&lt;/strong&gt;&lt;br&gt;
In most payment systems, there is a clear boundary between "deciding to pay" and "executing the payment." The payment intent captures the decision. The settlement adapter executes it.&lt;/p&gt;

&lt;p&gt;Cards, ACH, and instant rails already diverge at this boundary. A card adapter talks to a card processor, an ACH adapter formats NACHA files or calls a banking API, and an instant rail adapter connects to a real-time payment network. Despite the backend differences, they all receive the same input (a payment intent with amount, currency, and destination) and produce the same output (a settlement result with status and reference).&lt;/p&gt;

&lt;p&gt;Stablecoins hook in at exactly the same seam. The stablecoin adapter takes a payment intent and executes settlement by broadcasting a token transfer to the appropriate blockchain. It then watches for confirmation and reports back with a settlement result.&lt;/p&gt;

&lt;p&gt;That adapter interface in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SettlementAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;rail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;initiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentIntent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SettlementResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;checkStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SettlementStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;onFinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FinalityHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Your stablecoin adapter implements the same interface&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stablecoinAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SettlementAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;rail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stablecoin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;initiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentIntent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SettlementResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;broadcastTransfer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stablecoin&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;walletAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toSmallestUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stablecoin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stablecoin&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;checkStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SettlementStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getConfirmationCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getThresholdForChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;confirmations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SETTLED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;onFinality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;confirmationTracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adapter has the same shape as your card adapter and your ACH adapter. The rest of your system does not know or care which one is running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a Single Internal Payment State Machine&lt;/strong&gt;&lt;br&gt;
Do not introduce stablecoin-specific states. This is the fastest way to create branching logic everywhere in your codebase, from reporting to reconciliation to customer support tooling.&lt;/p&gt;

&lt;p&gt;Instead, map stablecoin finality events into your existing state machine:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Your Existing State&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Card Behavior&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ACH Behavior&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Stablecoin Behavior&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INITIATED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Intent created&lt;/td&gt;
&lt;td&gt;Intent created&lt;/td&gt;
&lt;td&gt;Intent created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PENDING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auth hold placed&lt;/td&gt;
&lt;td&gt;Submitted to bank&lt;/td&gt;
&lt;td&gt;Transaction broadcast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SETTLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Capture completed (clearing initiated)&lt;/td&gt;
&lt;td&gt;Bank confirms credit&lt;/td&gt;
&lt;td&gt;Confirmation threshold met&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Decline or timeout&lt;/td&gt;
&lt;td&gt;Return or rejection&lt;/td&gt;
&lt;td&gt;Transaction reverted or timed out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The state machine is rail-agnostic. Only the settlement adapter knows which rail is running. Your finance dashboard and reconciliation jobs work against the same states regardless of the underlying rail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route Settlement Based on Context&lt;/strong&gt;&lt;br&gt;
Settlement routing should depend on corridor, region, volume, or counterparty preference. There is no reason to treat stablecoin payments as a separate product category at the routing layer.&lt;/p&gt;

&lt;p&gt;A simplified routing function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;selectSettlementAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentIntent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;SettlementAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sourceCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;destinationCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;corridor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Cross-border treasury above threshold: use stablecoin rails&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;corridor&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cross-border&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stablecoin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Domestic payments: use local bank rails&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;corridor&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domestic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localBank&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Default: card rails&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to shift a corridor from bank rails to stablecoin rails, you update the routing config. You do not touch checkout, reporting, or reconciliation logic.&lt;/p&gt;

&lt;p&gt;One factor that makes stablecoin routing different from traditional rail routing: Gas costs are flat per transaction. While a &lt;a href="https://dev.to/flutterwaveeng/a-z-of-card-payments-with-flutterwave-v4-145e"&gt;card payment&lt;/a&gt; costs the merchant 1.5–3.5% regardless of amount, stablecoin network fees are flat per transaction, typically just cents, whether you're moving $100 or $100,000. That flat fee structure makes stablecoins attractive for high-value transactions where percentage-based fees add up. Your routing logic can factor this in. Route payments above a certain threshold through stablecoin rails where the cost advantage is most pronounced, and keep smaller payments on card or instant rails where the user experience is more familiar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat Stablecoin Settlement Like Instant Rails&lt;/strong&gt;&lt;br&gt;
Stablecoins settle quickly but asynchronously. They behave more like instant bank transfers than like card authorizations. A few differences to design around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stablecoins are push-based.&lt;/strong&gt; The sender initiates and signs the transfer. There is no "authorization hold" followed by a "capture." The money moves when the transaction is broadcast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stablecoins are irreversible on-chain.&lt;/strong&gt; There are no chargebacks. If you need to issue refunds, that is a separate transaction your application layer handles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stablecoins confirm in seconds to minutes.&lt;/strong&gt; But "broadcast" is different from "settled." A transaction can be broadcast and visible on-chain before it reaches your finality threshold. Design for speed without assuming immediacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, your system can route settlements through stablecoin rails. But there is a concern that trips up most teams: How do you know when a stablecoin payment is actually final?&lt;/p&gt;
&lt;h2&gt;
  
  
  Handling Settlement Finality Without Breaking Your Ledger
&lt;/h2&gt;

&lt;p&gt;The most common failure in stablecoin integration is updating balances too early. A transaction appears on-chain, an event fires, your system credits the recipient. Then the block gets reorganized, the transaction disappears, and your ledger is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define What "Final" Means for Your System&lt;/strong&gt;&lt;br&gt;
On traditional rails, finality is defined by the network. A card capture is final when the processor confirms it. An ACH credit is final after the settlement window closes. With stablecoins, you get to choose your finality threshold based on the blockchain network and your risk tolerance.&lt;/p&gt;

&lt;p&gt;Each network has different finality characteristics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Time to Finality&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Confirmation Approach&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum L1&lt;/td&gt;
&lt;td&gt;~13–19 minutes&lt;/td&gt;
&lt;td&gt;Wait for 2 finalized epochs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;~1–13 seconds&lt;/td&gt;
&lt;td&gt;"confirmed" (⅔ stake voted) or "finalized" (32 slots)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base / Arbitrum&lt;/td&gt;
&lt;td&gt;Seconds (soft) to ~15 minutes (hard)&lt;/td&gt;
&lt;td&gt;Sequencer provides instant soft finality; true finality inherits from Ethereum L1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tron&lt;/td&gt;
&lt;td&gt;~57 seconds&lt;/td&gt;
&lt;td&gt;19 of 27 Super Representatives confirm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polygon PoS&lt;/td&gt;
&lt;td&gt;~5 seconds&lt;/td&gt;
&lt;td&gt;Milestone-based finality post-Heimdall v2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most payment use cases, waiting for the chain's "finalized" commitment level is the right default. For lower-value transactions on networks with strong probabilistic guarantees (like Solana's "confirmed" level, which has historically shown extremely low rollback risk), you can accept faster confirmation.&lt;/p&gt;

&lt;p&gt;A practical approach is to scale your confirmation requirements with transaction value. For a $50 payment on Solana, "confirmed" commitment (about one to two seconds) is reasonable. For a $50,000 cross-border treasury movement on Ethereum L1, wait for full finality (two finalized epochs, roughly 13 minutes). This is the same risk-based approach you would use with any settlement rail. You would not wire $50,000 based on a pending ACH notification either.&lt;/p&gt;

&lt;p&gt;On Layer 2 networks like Base and Arbitrum, there are two layers of finality worth understanding. The sequencer provides soft finality almost instantly (under a second), which means the L2 has ordered and committed to including your transaction. Hard finality happens when the batch is posted to Ethereum L1 and that L1 block is finalized, which takes 13–19 minutes. For most payment scenarios, sequencer confirmation is sufficient because reverting it would require the sequencer to act maliciously, which carries substantial economic and reputational penalties. For very high-value settlements, wait for L1 finality.&lt;/p&gt;

&lt;p&gt;Pick a threshold, document it, and treat it as a contract between engineering and finance. Your finance team needs to know exactly when a stablecoin payment counts as settled in the books. If your threshold changes (say you move from Ethereum L1 to Base for a corridor), update the documentation and notify finance before the switch goes live. Surprises about when money is "real" erode trust fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate Confirmation Tracking from Balance Updates&lt;/strong&gt;&lt;br&gt;
Structure this as two distinct services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;confirmation tracker&lt;/strong&gt; watches the blockchain. It monitors transaction status, counts confirmations, and emits finality events when the threshold is reached. This service talks to RPC nodes or uses a webhook provider like &lt;a href="https://www.alchemy.com/" rel="noopener noreferrer"&gt;Alchemy&lt;/a&gt;, &lt;a href="https://www.quicknode.com/" rel="noopener noreferrer"&gt;QuickNode&lt;/a&gt;, or &lt;a href="https://tatum.io/" rel="noopener noreferrer"&gt;Tatum&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;ledger writer&lt;/strong&gt; updates balances. It listens for finality events and applies balance changes. It never talks to the blockchain directly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They communicate through events. This separation prevents race conditions and allows the confirmation tracker to handle retries and chain reorganizations without corrupting the ledger. If a reorganization happens and a previously confirmed transaction disappears, the confirmation tracker revokes the finality event. Depending on your system design, the ledger writer can reverse the balance update automatically, or flag it for manual review before reversal. Neither service needs to know how the other works internally, which makes each one easier to test and deploy independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for Idempotency and Retries&lt;/strong&gt;&lt;br&gt;
Webhook duplication and event replay are normal operating conditions with blockchain infrastructure. This is different from card processing, where you might see duplicates occasionally during edge cases. With blockchain &lt;a href="https://dev.to/flutterwaveeng/what-are-webhooks-and-how-do-you-implement-them-15j4"&gt;webhooks&lt;/a&gt;, expect them. Your system needs to handle them gracefully.&lt;/p&gt;

&lt;p&gt;Use the transaction hash as a natural idempotency key. It is globally unique and deterministic. Before processing any settlement event, check whether you have already processed that hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSettlementEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SettlementEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recipient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Idempotency check: skip if already processed&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByTxHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SETTLED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Already processed settlement for tx: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify confirmation threshold is met&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getConfirmationCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalityThreshold&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirmations&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Insufficient confirmations for tx: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Apply the balance update within a transaction&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SETTLED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;settledAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handle partial failure states explicitly. A broadcast can succeed while confirmation stalls (network congestion, gas price spikes). A confirmation can happen while your webhook delivery fails (your server was down). Build for both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconciliation as a First-Class Process&lt;/strong&gt;&lt;br&gt;
Real-time events are your primary settlement flow, but reconciliation catches everything that falls through the cracks. Treat it as a production process from day one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzrrq60gm6bsaol0eqrzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzrrq60gm6bsaol0eqrzf.png" alt="two reconciliation frequencies" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run scheduled reconciliation jobs that compare your internal ledger against on-chain state. For each stablecoin wallet your system manages, query the token's &lt;code&gt;balanceOf&lt;/code&gt; on-chain and compare it against the sum of credits and debits in your database. Flag any mismatches for investigation.&lt;/p&gt;

&lt;p&gt;Common causes of mismatches include missed webhook events, transactions that your system did not initiate (direct wallet transfers), failed transactions that consumed gas but did not transfer tokens, and events processed out of order during high-throughput periods.&lt;/p&gt;

&lt;p&gt;Build reconciliation at two frequencies. Real-time reconciliation runs on your critical path: When a high-value settlement event comes in, verify it against on-chain state before crediting the balance. Batch reconciliation runs on a schedule (hourly or daily): Query all managed wallet balances on-chain, and compare against your ledger totals. The batch job is your safety net. In practice, you will find mismatches early on, mostly from edge cases you did not anticipate. Each mismatch you investigate and resolve makes the system more reliable.&lt;/p&gt;

&lt;p&gt;If you are working with multiple stablecoin tokens (&lt;a href="https://www.circle.com/usdc" rel="noopener noreferrer"&gt;USDC&lt;/a&gt; and &lt;a href="https://tether.to/en/transparency" rel="noopener noreferrer"&gt;USDT&lt;/a&gt;, for example), be aware that they have different decimal precisions and transfer behaviors. USDC on Ethereum uses 6 decimals, while DAI uses 18. USDT's &lt;code&gt;transfer()&lt;/code&gt; function does not return a boolean value, which violates the ERC-20 specification and can cause issues if your smart contracts expect a standard return. Use a safe transfer library (like OpenZeppelin's &lt;code&gt;SafeERC20&lt;/code&gt;) if you are interacting with tokens at the contract level.&lt;/p&gt;

&lt;p&gt;This process is what builds trust with finance teams and passes audits. When finance asks "how do we know the stablecoin balances are correct?", your answer is reconciliation reports.&lt;/p&gt;

&lt;p&gt;That covers the technical patterns for settlement and finality. But shipping them to production is a different problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Safely Introduce Stablecoin Settlement
&lt;/h2&gt;

&lt;p&gt;The patterns above work. But shipping them to production requires a controlled rollout strategy. Do not skip this step. The difference between a successful stablecoin rollout and a rollback is almost always the rollout plan itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with Internal or Low-Risk Flows&lt;/strong&gt;&lt;br&gt;
Pick a flow where you control both sides of the transaction. Treasury movements between your own wallets, intercompany transfers, or a limited cross-border corridor with a trusted counterparty.&lt;/p&gt;

&lt;p&gt;Cross-border treasury is often the best starting point. The cost savings are immediately measurable; moving $100,000 between entities via correspondent banking might cost $3,000–$7,000 in fees and take three to five days, whereas a stablecoin transfer costs pennies and settles in minutes. Plus, the transaction volume is low enough to monitor manually during the early rollout. You also get real operational data on confirmation latency, gas costs, and reconciliation accuracy before any customer money is on the line.&lt;/p&gt;

&lt;p&gt;If something breaks on an internal treasury movement, it is an operational issue you can fix quietly. If something breaks on a customer-facing payment, it is a support ticket and a trust problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Feature Flags and Volume Caps&lt;/strong&gt;&lt;br&gt;
Place feature flags at the routing layer so you can disable stablecoin settlement with a config change. Enforce volume caps before settlement execution to limit your exposure while you build confidence.&lt;/p&gt;

&lt;p&gt;A practical rollout sequence: Start with 100% of internal treasury flows. Once stable, open to 1% of eligible cross-border payments. Ramp to 5%, then 25%, then 100% as your confidence in confirmation tracking and error handling grows.&lt;/p&gt;

&lt;p&gt;Build a circuit breaker into the routing layer. If stablecoin settlements fail above a configured threshold (say, five consecutive failures or a 10% failure rate over five minutes), automatically route traffic back to bank rails and alert the on-call engineer. You can retry stablecoin rails after a cooldown period. This is the same pattern you would use for any external dependency in a payment flow, and it means network congestion, RPC failures, or degraded confirmation times don't turn into customer-facing outages for your platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make Observability a Launch Requirement&lt;/strong&gt;&lt;br&gt;
If you cannot observe it, do not ship it. The metrics that matter most for stablecoin settlement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirmation latency (p50 and p95):&lt;/strong&gt; How long between broadcast and finality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed settlement rate:&lt;/strong&gt; Percentage of broadcasts that do not reach finality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation mismatch count:&lt;/strong&gt; Discrepancies between your ledger and on-chain state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gas cost per transaction:&lt;/strong&gt; Actual network fees paid per settlement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configure alerts for confirmation latency exceeding your defined threshold, settlement failure rates above your baseline, and any reconciliation drift detected during scheduled jobs.&lt;/p&gt;

&lt;p&gt;A useful practice is to build a per-rail dashboard that shows acceptance rates, average confirmation times, costs per transaction, and reconciliation status side-by-side for each settlement rail. This gives your team a single view to compare stablecoin performance against bank and card rails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Align with Finance Early&lt;/strong&gt;&lt;br&gt;
Your finance team does not care about blockchain technology. They care about when money is "really there," how they audit it, and what happens when something goes wrong.&lt;/p&gt;

&lt;p&gt;Walk them through your state diagrams. Show them what &lt;code&gt;INITIATED&lt;/code&gt;, &lt;code&gt;PENDING&lt;/code&gt;, and &lt;code&gt;SETTLED&lt;/code&gt; mean in practice. Explain your finality rules in their language: "We count a stablecoin payment as settled when the network has confirmed it to a point where reversal would require destroying billions of dollars in collateral." Show them reconciliation reports. Give them access to the same dashboard your engineering team uses so they can see settlement status in real time.&lt;/p&gt;

&lt;p&gt;Finance cares about predictability. Lead with system behavior. If you can demonstrate that stablecoin settlement produces the same ledger entries, the same reconciliation reports, and the same audit trail as your existing rails, the conversation shifts from "should we do this?" to "which corridors should we roll this out to next?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Stablecoin integration is a routing and settlement problem, and teams that treat it that way integrate faster and ship safer. The adapter pattern, unified state machine, and confirmation tracking covered here are the same patterns you would use to add any new settlement rail. Stablecoins just happen to settle faster and work around the clock.&lt;/p&gt;

&lt;p&gt;If you are looking to integrate stablecoin settlement into your own platform, &lt;a href="https://flutterwave.com" rel="noopener noreferrer"&gt;Flutterwave&lt;/a&gt;, has &lt;a href="https://flutterwave.com/us/blog/flutterwave-partners-with-polygon-as-the-primary-blockchain-partner-for-cross-border-payments" rel="noopener noreferrer"&gt;integrated Polygon-based&lt;/a&gt; stablecoin settlement into its platform, with a broader rollout planned across its merchant base throughout 2026.&lt;/p&gt;

&lt;p&gt;Start with one corridor and one flow. Measure everything. The infrastructure patterns are proven, the architecture is familiar, and the only new thing is the settlement rail itself.&lt;/p&gt;

</description>
      <category>stablecoin</category>
      <category>payment</category>
      <category>rails</category>
      <category>fintech</category>
    </item>
    <item>
      <title>What Are Stablecoins? Understand How They Work</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 20 Feb 2026 10:53:03 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/what-are-stablecoins-understand-how-they-work-3kg9</link>
      <guid>https://forem.com/flutterwaveeng/what-are-stablecoins-understand-how-they-work-3kg9</guid>
      <description>&lt;p&gt;In 2025, stablecoins processed over &lt;a href="https://www.bloomberg.com/news/articles/2026-01-08/stablecoin-transactions-rose-to-record-33-trillion-led-by-usdc?embedded-checkout=true" rel="noopener noreferrer"&gt;$33 trillion USD&lt;/a&gt; in transaction volume. Names like USDT and USDC have become common across crypto exchanges, fintech apps, and cross-border payment platforms. Yet most explanations treat stablecoins like speculative digital assets rather than what they actually are: a new type of payment rail built on blockchain technology.&lt;/p&gt;

&lt;p&gt;This article closes that gap. You will learn what stablecoins are, how they maintain price stability, and how they function as settlement rails in production systems. By the end, you will be able to reason about stablecoins the way you reason about ACH or SWIFT: as payment rails with specific characteristics, tradeoffs, and appropriate use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Stablecoins?
&lt;/h2&gt;

&lt;p&gt;A stablecoin is a digital currency designed to maintain a stable value relative to a reference asset, typically a fiat currency like the US dollar. Unlike Bitcoin or Ethereum, which can swing 10% or more in a single day due to price fluctuations, stablecoins aim to hold a consistent 1:1 peg with their reference currency. A stablecoin pegged to the dollar, for example, targets a stable price of exactly $1.00.&lt;/p&gt;

&lt;p&gt;Think of it like a digital representation of a bank deposit. When a company like Circle (the issuer behind USDC) receives a dollar deposit through a regulated financial institution, they mint a USDC token on the blockchain, similar to how a bank credits your account when you wire funds in. When someone redeems their USDC, Circle burns the token and wires the dollar back. This mint-and-burn cycle, backed by cash and US treasuries, is what keeps one USDC worth one dollar. The difference from traditional banking is that these balances live on a public ledger and can move 24/7 without waiting for bank processing windows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc3hf9hxchquyd5n9ejen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc3hf9hxchquyd5n9ejen.png" alt="Stablecoin mint &amp;amp; burn cycle" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a developer, the distinction is operational. You cannot price a merchant invoice in Bitcoin because the value might fluctuate 5% between the time the invoice is generated and the time the transaction settles. A stablecoin solves this volatility problem while retaining the core benefits of blockchain infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;24/7 Availability:&lt;/strong&gt; No banking holidays or downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programmability:&lt;/strong&gt; Smart contracts can automate flows based on logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Reach:&lt;/strong&gt; The network is agnostic to borders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we discuss stablecoins in the context of payments infrastructure, we are almost exclusively referring to tokens that tokenize fiat currency on a public ledger. They act as a bridge, importing the stability of national currencies into a public and verifiable blockchain.&lt;/p&gt;

&lt;p&gt;Over 90% of stablecoin market capitalization today is fiat-backed, with nearly all of that pegged to the US dollar. The total stablecoin market exceeded $200 billion by early 2025, with &lt;a href="https://tether.to/" rel="noopener noreferrer"&gt;Tether (USDT)&lt;/a&gt; holding the largest share at over $140 billion and &lt;a href="https://www.circle.com/en/usdc" rel="noopener noreferrer"&gt;Circle's USDC&lt;/a&gt; at approximately $44 billion.&lt;/p&gt;

&lt;p&gt;But market size alone doesn't tell you whether you can rely on these tokens in production. For that, you need to understand how the $1.00 peg actually holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Fiat-Backed Stablecoins Maintain a Stable Value
&lt;/h2&gt;

&lt;p&gt;Sending a stablecoin and having it arrive is the easy part. The harder question is: Why does the price stay at $1.00, and what happens when it doesn't? For developers building payment systems, the answer directly affects how you handle settlement finality, operational risk, and failure modes. The stability of a stablecoin is the result of specific mechanisms involving reserves, issuers, and market incentives.&lt;/p&gt;

&lt;p&gt;Understanding these mechanics affects how you handle settlement finality, operational risk, and failure modes in your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fiat Reserves, Bank Deposits, and Custodians
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff49cdmchslqnhwl82vji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff49cdmchslqnhwl82vji.png" alt="fiat backed stablecoin" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The majority of the stablecoin market operates on a fiat-backed model. By market capitalization, over 90% of stablecoins fall into this category. This is the model most relevant to enterprise payments.&lt;/p&gt;

&lt;p&gt;In this architecture, a centralized issuer (such as Circle for USDC or Tether for USDT) mints stablecoins only when equivalent fiat value is deposited into their reserve accounts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Reserve:&lt;/strong&gt; The stablecoin issuer holds reserve assets, including cash, US Treasury bills (government debt instruments), money market funds, and other liquid assets in regulated financial institutions. These stable assets back every token in circulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Claim:&lt;/strong&gt; The stablecoin token represents a redemption right; the issuer is contractually obligated to exchange it back for dollars.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For developers building treasury or payment systems, this means you treat stablecoin balances as cash equivalents only if the redemption path is reliable. The risk here is not code risk from a smart contract bug. It is counterparty risk. If a custodian holding reserve assets fails, or if the issuer faces regulatory action, the 1:1 backing can be threatened.&lt;/p&gt;

&lt;p&gt;Your system must treat stablecoin balances differently than native crypto assets. You are not just trusting the blockchain. You are trusting the off-chain bridge to real dollars. Say you're building a payroll platform that holds contractor funds in USDC before disbursement. A custodian failure means your contractors don't get paid. Your system needs to track issuer health the same way you'd monitor a banking partner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issuance and Redemption Cycles
&lt;/h3&gt;

&lt;p&gt;The supply of a stablecoin is elastic. It expands and contracts based on liquidity needs through minting and burning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minting (fiat → stablecoin):&lt;/strong&gt; An institutional customer wires USD to the issuer. The issuer verifies receipt and calls a mint function on the stablecoin smart contract, creating new tokens and sending them to the customer's wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burning (stablecoin → fiat):&lt;/strong&gt; A customer sends stablecoins to the issuer's redemption address. The issuer calls a burn function to destroy the tokens on-chain and wires the equivalent USD back to the customer's bank account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This elastic supply is what keeps the peg intact at scale. When demand rises, new tokens are minted to meet it. When holders want to exit, tokens are burned, and supply shrinks. The system self-corrects as long as the issuer can honor redemptions. When they can't, things break fast.&lt;/p&gt;

&lt;p&gt;In March 2023, Silicon Valley Bank collapsed with &lt;a href="https://www.coindesk.com/business/2023/03/11/circle-confirms-33b-of-usdcs-cash-reserves-stuck-at-failed-silicon-valley-bank" rel="noopener noreferrer"&gt;$3.3 billion&lt;/a&gt; of Circle's USDC reserves inside. USDC de-pegged to below $0.88 before banking access was restored. Payment platforms with no monitoring for issuer-level events continued accepting USDC at face value, and their ledgers didn't match reality.&lt;/p&gt;

&lt;p&gt;Your payment system should detect issuer-level incidents, not just blockchain network failures. If the issuer halts redemptions, your system might need to pause acceptance or adjust risk parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Market Arbitrage Enforcing the Peg
&lt;/h3&gt;

&lt;p&gt;The issuer does not constantly intervene in the market to fix the price at $1.00. Instead, they rely on a distributed network of arbitrageurs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If price &amp;lt; $1.00:&lt;/strong&gt; Arbitrageurs buy the stablecoin on exchanges for $0.99, redeem it with the issuer for $1.00, and pocket the difference. This buying pressure pushes the price back up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If price &amp;gt; $1.00:&lt;/strong&gt; Arbitrageurs mint new stablecoins from the issuer for $1.00 and sell them on exchanges for $1.01. This selling pressure pushes the price back down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two forces help maintain price alignment without anyone actively managing it. Whenever the price drifts from $1.00, traders step in because there's money to be made by correcting it. The peg holds not because someone is controlling it, but because it's profitable to fix it when it breaks.&lt;/p&gt;

&lt;p&gt;But this mechanism depends on arbitrageurs having access to liquidity. During extreme market stress, if they can't access capital (for example, when banking rails are closed on weekends), the peg might temporarily wobble. Think about an invoicing platform where merchants generate USDC-denominated invoices. If the peg drops to $0.995 on a weekend and your system auto-accepts at $1.00, you're eating the difference on every transaction. A simple price feed check before accepting payment can prevent this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fiat-Backed vs Algorithmic Models
&lt;/h3&gt;

&lt;p&gt;While fiat-backed stablecoins dominate, you may encounter algorithmic stablecoins. These attempt to maintain a peg through on-chain incentives and smart contracts that manipulate supply, often backed by volatile crypto assets rather than fiat.&lt;/p&gt;

&lt;p&gt;The problem with this approach showed up clearly in May 2022 when &lt;a href="https://www.sciencedirect.com/science/article/abs/pii/S1544612322007668" rel="noopener noreferrer"&gt;TerraUST collapsed&lt;/a&gt;. Terra relied on its companion token LUNA to absorb price fluctuations, but when confidence dropped, both tokens entered a death spiral. Over $40 billion in value was wiped out in days. The peg wasn't backed by dollars in a vault. It was backed by market confidence, and once that broke, there was nothing underneath.&lt;/p&gt;

&lt;p&gt;Payment infrastructure requires a deterministic value. You cannot build a reliable settlement rail on a token that relies on game theory to hold its value. Stick to fiat-backed stablecoins for production payment flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transparency and Attestations
&lt;/h3&gt;

&lt;p&gt;How do you know the reserves actually exist? Unlike public blockchains, where every transaction is visible, off-chain reserves are opaque. Issuers address this through attestations: periodic reports by third-party accounting firms verifying that assets exceed liabilities.&lt;/p&gt;

&lt;p&gt;Circle publishes monthly attestation reports from Deloitte that detail exactly what assets back each USDC token in circulation. Tether publishes quarterly reports from BDO Italia with reserve breakdowns.&lt;/p&gt;

&lt;p&gt;For compliance and finance teams, these attestations are the difference between an internal accounting nightmare and a compliant instrument. When integrating a stablecoin, your compliance team will likely require these reports before approving the asset for treasury operations. Build this into your vendor evaluation process.&lt;/p&gt;

&lt;p&gt;Once you're confident in the token's backing, the next question is: How does a stablecoin transaction actually move from sender to receiver?&lt;/p&gt;

&lt;h2&gt;
  
  
  How Stablecoin Settlement Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0mdjc7advujt09zsc9h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0mdjc7advujt09zsc9h.jpg" alt="stablecoin transaction lifecycle" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stablecoin transactions settle on blockchain networks, which means understanding blockchain mechanics is necessary for building reliable payment systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction Lifecycle
&lt;/h3&gt;

&lt;p&gt;When you initiate a stablecoin transfer, the transaction follows a specific path. Your application broadcasts the signed transaction to the network. The transaction enters the mempool, where pending transactions wait until validators include them in a block. Once included, the transaction has its first confirmation.&lt;/p&gt;

&lt;p&gt;But one confirmation is not settlement. As more blocks are added, the transaction becomes increasingly difficult to reverse. After enough confirmations, the exact number depends on the network and your risk tolerance, the transaction reaches finality. At that point, the balance update is irreversible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Differences
&lt;/h3&gt;

&lt;p&gt;Settlement speed varies across blockchain networks. On Ethereum, blocks are produced every 12 seconds, but true finality takes approximately 12–15 minutes. Faster networks like &lt;a href="https://solana.com/" rel="noopener noreferrer"&gt;Solana&lt;/a&gt; achieve fast confirmation times, often under one second, with full finality around 12 seconds. &lt;a href="https://tron.network/" rel="noopener noreferrer"&gt;Tron&lt;/a&gt;, which hosts significant USDT volume, produces blocks every three seconds with practical finality in around one minute.&lt;/p&gt;

&lt;p&gt;Layer 2 networks like &lt;a href="https://base.org/" rel="noopener noreferrer"&gt;Base&lt;/a&gt; and &lt;a href="https://polygon.technology/" rel="noopener noreferrer"&gt;Polygon&lt;/a&gt; offer faster confirmation times with lower fees, though they inherit security guarantees from their underlying Layer 1 chain. &lt;a href="https://flutterwave.com/us/stablerails" rel="noopener noreferrer"&gt;Flutterwave's&lt;/a&gt; stablecoin infrastructure runs on Polygon, which provides sub-second confirmations and transaction fees that typically stay under $0.01. Choose your network based on the tradeoffs that matter for your use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Irreversibility and Fees
&lt;/h3&gt;

&lt;p&gt;Unlike card payments or ACH transfers, blockchain transactions cannot be reversed once finalized. There is no chargeback mechanism, no dispute process at the protocol level, and no way to claw back funds sent to the wrong address.&lt;/p&gt;

&lt;p&gt;This irreversibility eliminates chargeback fraud risk almost entirely, which costs merchants billions annually on card networks. But it also means user errors are unrecoverable without the recipient's cooperation. Your application layer must handle refunds because the blockchain will not.&lt;/p&gt;

&lt;p&gt;Transaction fees are paid in the network's native currency rather than as a percentage of the transfer amount. Fees vary based on network congestion, from under $1 on Layer 2 networks to $50 or more on Ethereum during high-demand periods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stablecoins vs Traditional Payment Rails
&lt;/h2&gt;

&lt;p&gt;Stablecoins compete architecturally with ACH and SWIFT, not with Visa and Mastercard. Card networks handle authorization and routing, while final settlement occurs later through banking rails. Stablecoins handle settlement directly on-chain. &lt;/p&gt;

&lt;p&gt;Understanding this distinction clarifies where integration makes sense.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Characteristic&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Stablecoins&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ACH&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;SWIFT&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cards&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Settlement Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Seconds to minutes&lt;/td&gt;
&lt;td&gt;1-3 business days&lt;/td&gt;
&lt;td&gt;1-5 business days&lt;/td&gt;
&lt;td&gt;1-3 business days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operating Hours&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;24/7/365&lt;/td&gt;
&lt;td&gt;Business days only&lt;/td&gt;
&lt;td&gt;Business days only&lt;/td&gt;
&lt;td&gt;24/7 authorization, batch settlement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.01-$5 network fee&lt;/td&gt;
&lt;td&gt;$0.20-$1.50&lt;/td&gt;
&lt;td&gt;$25-50+&lt;/td&gt;
&lt;td&gt;1.5-3.5% of transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Finality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Irreversible&lt;/td&gt;
&lt;td&gt;60-day return window&lt;/td&gt;
&lt;td&gt;Reversible via investigation&lt;/td&gt;
&lt;td&gt;120-day chargeback window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geographic Reach&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;US domestic&lt;/td&gt;
&lt;td&gt;Global with correspondent banks&lt;/td&gt;
&lt;td&gt;Global with network acceptance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The comparison reveals complementary strengths. Stablecoins excel at cross-border transfers where traditional correspondent banking adds days of delay and percentage-point fees. ACH remains superior for domestic US payments where low cost and established integration outweigh speed benefits. Cards provide consumer protection through chargebacks that stablecoins cannot match.&lt;/p&gt;

&lt;p&gt;Choose based on your use case. A platform paying international contractors benefits from stablecoin rails. A consumer e-commerce checkout benefits from card acceptance with its buyer protections.&lt;/p&gt;

&lt;p&gt;If stablecoin rails are the right fit for your use case, the next step is understanding what integration actually looks like at the system level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Stablecoins in Your Payment Flows
&lt;/h2&gt;

&lt;p&gt;Adding stablecoin support to a payment platform involves wallet management, transaction initiation, confirmation handling, and reconciliation. While detailed implementation guidance deserves its own treatment, understanding the high-level patterns helps you evaluate the engineering investment required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wallet Management
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fky1pos3eyv7bww2ezvqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fky1pos3eyv7bww2ezvqc.png" alt="wallet architecture" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your system needs a way to receive funds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Segregated Wallets:&lt;/strong&gt; You generate a unique blockchain address for every customer or deposit session. This makes reconciliation easy (Address A = Customer A), but gas costs are higher because you eventually have to "sweep" funds to a central treasury.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Omnibus Wallets:&lt;/strong&gt; You use one central address for all inflows. Customers must include a "memo" or unique identifier in the transaction metadata. This is efficient but prone to user error (users forgetting the memo).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  On-Chain Event Monitoring
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2r4i6j0t5qlayc0x59un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2r4i6j0t5qlayc0x59un.png" alt="on-Chain event monitoring flow" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You cannot rely on webhooks from a third party alone; robust systems run their own "listeners." &lt;/p&gt;

&lt;p&gt;A typical flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listen:&lt;/strong&gt; Service watches the blockchain for &lt;code&gt;Transfer&lt;/code&gt; events to your deposit address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter:&lt;/strong&gt; Check if the token contract matches the official stablecoin address (preventing fake token scams).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count:&lt;/strong&gt; Wait for &lt;code&gt;X&lt;/code&gt; confirmations (e.g., 12 blocks).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile:&lt;/strong&gt; Update the user's balance in your database.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Handling Failed or Stuck Transactions
&lt;/h3&gt;

&lt;p&gt;Blockchain transactions can fail (e.g., out of gas) or remain pending if the offered gas fee is too low. Your UI must account for this pending state. Unlike a declined credit card which is instant, a pending blockchain transaction might time out after hours. Your system needs logic to detect these zombies and prompt the user to retry or speed up the transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Risks and Limitations
&lt;/h2&gt;

&lt;p&gt;Building with stablecoins requires clear-eyed assessment of the risks they introduce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custody Risk
&lt;/h3&gt;

&lt;p&gt;Custody risk means that whoever holds the private keys controls the assets. Compromised keys mean lost funds with no recovery mechanism. Production systems require enterprise-grade security: hardware security modules, multi-signature schemes, and strict access controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issuer Risk
&lt;/h3&gt;

&lt;p&gt;Issuer risk acknowledges that stablecoins are only as reliable as their issuers. If an issuer mismanages reserves, faces regulatory action, or experiences operational failures, your stablecoin balances are affected. Diversifying across compliant issuers and monitoring issuer health reduces but does not eliminate this risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regulatory Exposure
&lt;/h3&gt;

&lt;p&gt;Regulatory exposure varies by jurisdiction and continues to change. Some regions have clear frameworks for stablecoin usage; others have ambiguous or restrictive rules. Your compliance team must evaluate the regulatory status of stablecoin operations in every market you serve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Congestion
&lt;/h3&gt;

&lt;p&gt;Network congestion can spike transaction fees and delay confirmations during high-demand periods. Your system should handle variable fees gracefully and set appropriate expectations for users about settlement timing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Irreversibility
&lt;/h3&gt;

&lt;p&gt;Irreversibility shifts all refund responsibility to your application layer. You cannot rely on the payment network to reverse fraudulent or erroneous transactions.&lt;/p&gt;

&lt;p&gt;These risks do not disqualify stablecoins from serious payment infrastructure. They require the same thoughtful risk management you apply to any financial system dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Stablecoins Fit in Modern Payment Architectures
&lt;/h2&gt;

&lt;p&gt;Stablecoins are not a hammer for every nail. They fit best in specific architectural gaps where traditional rails struggle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Border Treasury Movement
&lt;/h3&gt;

&lt;p&gt;Moving funds between subsidiaries across Nigeria, the UK, and the US via SWIFT is slow and expensive, with fees exceeding 8% in some African countries and settlement taking days. Stablecoins allow for near-instant treasury rebalancing, freeing up working capital that would otherwise be stuck in transit. This is already happening at scale. &lt;a href="https://flutterwave.com/us/stablerails" rel="noopener noreferrer"&gt;Flutterwave&lt;/a&gt;, for example, built a stablecoin-powered cross-border payment network across 30 African countries, settling transactions in seconds instead of days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merchant Settlement
&lt;/h3&gt;

&lt;p&gt;For merchants selling internationally, waiting as much as T+2 days for settlement ties up cash flow. With stablecoin settlement, merchants can receive funds in minutes. Flutterwave's recently launched stablecoin balances let merchants hold and transact in USDC and USDT alongside traditional currencies like USD and NGN, giving them the option to settle on whichever rail best fits the transaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Liquidity Bridging
&lt;/h3&gt;

&lt;p&gt;Fintechs can use stablecoins to bridge liquidity between different fiat currencies. Rather than pre-funding accounts in every target currency, companies can hold liquidity in stablecoins and swap into local fiat only when a payment needs to be made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For developers building payment systems, understanding stablecoins is now part of the job. You need to recognize that stablecoin rails offer specific advantages for specific use cases: faster cross-border settlement, lower fees for international transfers, and 24/7 operation without banking hour constraints.&lt;/p&gt;

&lt;p&gt;The technology continues to mature. Regulatory frameworks are crystallizing in major jurisdictions. Enterprise adoption is accelerating. Platforms like &lt;a href="https://flutterwave.com/us/stablerails" rel="noopener noreferrer"&gt;Flutterwave&lt;/a&gt; are demonstrating how stablecoin infrastructure integrates with traditional payment methods to serve merchant needs across emerging markets.&lt;/p&gt;

&lt;p&gt;Explore how &lt;a href="https://flutterwave.com/us/stablerails" rel="noopener noreferrer"&gt;stablecoin settlement&lt;/a&gt; fits into your payment architecture.&lt;/p&gt;

</description>
      <category>stablecoins</category>
      <category>usdt</category>
      <category>fintech</category>
      <category>polygon</category>
    </item>
    <item>
      <title>B2B Payments: Best Practices to Secure Payment Processing</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Mon, 19 Jan 2026 11:50:04 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/b2b-payments-best-practices-to-secure-payment-processing-241m</link>
      <guid>https://forem.com/flutterwaveeng/b2b-payments-best-practices-to-secure-payment-processing-241m</guid>
      <description>&lt;p&gt;Your finance team just approved a $50,000 payment to a supplier. The wire transfer goes through smoothly. Monday morning, your supplier calls asking where their payment is. Your bank confirms the money left your account, but it went to fraudsters who intercepted the payment details through a business email compromise attack. The money is gone, and you're now facing angry suppliers, compliance investigations, and a board that wants answers.&lt;/p&gt;

&lt;p&gt;If you're processing B2B payments, you're dealing with risks that dwarf typical consumer financial transactions. While the story above highlights wire fraud, attackers target every payment rail available, from corporate credit cards to virtual accounts. Unlike a $50 purchase at an online store, B2B payments involve large sums that make them attractive targets for sophisticated fraud.&lt;/p&gt;

&lt;p&gt;Recent data from the &lt;strong&gt;2025 AFP Payments Fraud and Control Survey&lt;/strong&gt; shows that &lt;a href="https://www.financialprofessionals.org/training-resources/resources/survey-research-economic-data/details/payments-fraud" rel="noopener noreferrer"&gt;79%&lt;/a&gt; of organizations were targets of payment fraud in 2024. Add multiple approval chains, various stakeholders, complex payment methods, and strict compliance requirements, and you have a payment ecosystem where a single payment security gap can cost you more than your annual revenue.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how to build secure B2B payment systems that protect your transactions from fraud while meeting compliance standards. By the end, you'll know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to authenticate high-risk card payments using 3D Secure 2 to shift fraud liability&lt;/li&gt;
&lt;li&gt;When and how to implement tokenization to protect payment information in recurring B2B payments&lt;/li&gt;
&lt;li&gt;How virtual accounts simplify reconciliation while providing a secure payment method&lt;/li&gt;
&lt;li&gt;Methods to monitor suspicious activity in real-time before money leaves your account&lt;/li&gt;
&lt;li&gt;Data protection practices that keep sensitive information secure throughout the payment flow&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why B2B Payments Need Extra Security
&lt;/h2&gt;

&lt;p&gt;The attack surface and potential impact of a B2B payment failure are much larger than failures in the consumer space. Here is what makes the risks so much higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction size matters:&lt;/strong&gt; When you're processing $100,000 invoices instead of $100 purchases, fraud becomes more lucrative. Attackers know this and specifically target B2B payment flows. They gravitate toward legacy fraud methods where large sums are most vulnerable. This is why check fraud remains the top source of B2B payment losses, because a single compromised check can drain significant funds before anyone notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple stakeholders create more attack vectors:&lt;/strong&gt; A typical B2B payment might involve your procurement team, finance department, the vendor's sales team, their accounting department, and various approval workflows. Each person and system in that chain is a potential entry point for attackers. Business email compromise (&lt;a href="https://www.fbi.gov/how-we-can-help-you/scams-and-safety/common-frauds-and-scams/business-email-compromise" rel="noopener noreferrer"&gt;BEC&lt;/a&gt;) attacks exploit exactly this complexity by impersonating executives or vendors to redirect payments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data risk:&lt;/strong&gt; The data being exchanged is not just a credit card number. It includes sensitive business information, financial records, and vendor/client lists. A breach can lead to devastating, cascading consequences: immediate financial loss, severe regulatory fines for non-compliance with standards like PCI DSS or data-privacy laws, and a permanent loss of trust that can destroy a business's reputation.&lt;/p&gt;

&lt;p&gt;The stakes are clear: large transactions make you a target, multiple stakeholders create more entry points for attackers, and the sensitive data flowing through your systems demands protection at every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices for Secure B2B Payment Processing
&lt;/h2&gt;

&lt;p&gt;There is no single "silver bullet" for secure payment processing. A resilient B2B payment architecture is built on a layered security model, where each principle and practice is designed to defend against a specific attack vector. As an engineering lead or developer, your role is to architect these layers, building a system that is secure by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Authenticate High-Risk Credit Card Payments with 3D Secure 2&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://flutterwave.com/us/blog/everything-you-need-to-know-about-3d-secure-2" rel="noopener noreferrer"&gt;3D Secure 2&lt;/a&gt; (3DS2) adds a critical authentication layer for credit card payments by verifying that the person making the online transaction actually owns the card. It's one of the most effective authentication methods for online payment security.&lt;/p&gt;

&lt;p&gt;Here's how it works: when processing a payment, 3DS2 transmits over 100 data points to the cardholder's bank, including device ID, transaction history, and shipping address. The bank uses this information to assess risk, triggering one of two flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frictionless Flow:&lt;/strong&gt; For low-risk transactions, the payment proceeds immediately without any user interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge Flow:&lt;/strong&gt; For higher-risk payments, the cardholder must authenticate using their banking app, biometrics (fingerprint or facial recognition), or an OTP.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2cmyifn2pn2mimj7ivx.png" alt="3DS2 payment authentication flow" width="800" height="756"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How Flutterwave Can Help&lt;/strong&gt;&lt;br&gt;
Flutterwave supports &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/direct-card-charge#external-3ds" rel="noopener noreferrer"&gt;3DS2 automatically&lt;/a&gt; through payment APIs and Checkout. When you initiate a card charge, Flutterwave detects if 3DS2 is required and handles the authentication flow. If the card requires 3DS authentication, the response includes a redirect URL where customers complete verification with their bank.&lt;/p&gt;

&lt;p&gt;The security features here are substantial: 3DS2 shifts chargeback liability for fraudulent transactions (like stolen credit card usage) from you to the cardholder's bank. If someone uses a stolen card and passes 3DS2 authentication, the financial institution bears the loss, not your business. This makes 3DS2 a core part of any payment security strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Prevent Data Breaches with Payment Tokenization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcz65kvr4prailib698uk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcz65kvr4prailib698uk.png" alt="How Tokenization Works" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Storing card details for recurring B2B payments is both a security risk and a compliance headache. &lt;a href="https://dev.to/flutterwaveeng/tokenization-vs-encryption-for-payment-data-security-1185"&gt;Tokenization&lt;/a&gt; solves both problems.&lt;/p&gt;

&lt;p&gt;Instead of storing actual card numbers, tokenization replaces them with unique tokens that represent the customer's payment method. Even if intercepted, these tokens are worthless to attackers because they cannot be reverse-engineered into card details or used to transact on any other platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Flutterwave Can Help&lt;/strong&gt;&lt;br&gt;
Here is the step-by-step developer flow for securely implementing recurring B2B payments using Flutterwave's &lt;a href="https://developer.flutterwave.com/v3.0/docs/tokenization" rel="noopener noreferrer"&gt;tokenization&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The Initial Charge:&lt;/strong&gt; First, you must charge the customer one time to create the token. This is typically done using a secure, hosted solution. We recommend &lt;a href="https://developer.flutterwave.com/v3.0/docs/inline" rel="noopener noreferrer"&gt;Flutterwave Inline&lt;/a&gt;, which isolates your systems from the card data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Verify and Extract Token:&lt;/strong&gt; After the customer completes the initial payment, your backend must call Flutterwave's &lt;code&gt;verify&lt;/code&gt; endpoint for that transaction. In the successful &lt;code&gt;JSON&lt;/code&gt; response, you will find the &lt;code&gt;token&lt;/code&gt; field nested within the &lt;code&gt;data.card&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;A sample verification response might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transaction fetched successfully"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;285959875&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tx_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_TX_REF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"successful"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-b2b@example.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"first_6digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"553088"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"last_4digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2950"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flw-t1nf-93da56b24f8ee332304cd2eea40a1fc4-m03k"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Store the Token:&lt;/strong&gt; In your backend database, you will now save this token: &lt;code&gt;flw-t1nf-93da56b24f8ee332304cd2eea40a1fc4-m03k&lt;/code&gt;. Link this token to the customer's profile using their unique user ID and the email address used for the charge. You can also store the non-sensitive fields like &lt;code&gt;last_4digits&lt;/code&gt; and &lt;code&gt;issuer&lt;/code&gt; to display in your application's "Payment Methods" UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Subsequent Charges.&lt;/strong&gt; For all future recurring payments, your server will make a secure, server-to-server API call to the tokenized charge endpoint. You do not need the customer to be present. Here is the API endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.flutterwave.com/v3/tokenized-charges
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will pass the required parameters in the request body, including the &lt;code&gt;token&lt;/code&gt; you stored, the customer's &lt;code&gt;email&lt;/code&gt;, the &lt;code&gt;amount&lt;/code&gt;, and a unique &lt;code&gt;tx_ref&lt;/code&gt; for this new transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flw-t1nf-93da56b24f8ee332304cd2eea40a1fc4-m03k"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-b2b@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tx_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recurring-sub-b2b-002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NG"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API-driven flow ensures you can manage complex B2B billing cycles while architecturally eliminating the risk and compliance overhead of handling sensitive cardholder data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Use Virtual Accounts for B2B Collections&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://dev.to/flutterwaveeng/understanding-virtual-accounts-with-flutterwave-4mnm"&gt;Virtual accounts&lt;/a&gt; give each customer or transaction a unique bank account number for payments. This simple feature dramatically improves both security and reconciliation.&lt;/p&gt;

&lt;p&gt;When you create a virtual account through Flutterwave, you get a real bank account number that routes payments to your business. Because each virtual account is unique, you instantly know which customer or invoice a payment relates to without manual matching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz87jghdipl7tbspphrfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz87jghdipl7tbspphrfx.png" alt="Virtual Account Reconciliation Flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Flutterwave Can Help&lt;/strong&gt;&lt;br&gt;
Flutterwave offers two types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic virtual accounts&lt;/strong&gt; expire after one hour or first use. Perfect for one-time invoices where you need automatic reconciliation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static virtual accounts&lt;/strong&gt; are permanent, tied to specific customers. Ideal for recurring B2B relationships where the same client makes regular payments.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a static virtual account&lt;/span&gt;
curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s1"&gt;'https://api.flutterwave.com/v3/virtual-account-numbers'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer YOUR_SECRET_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-raw&lt;/span&gt; &lt;span class="s1"&gt;'{
    "email": "[email protected]",
    "is_permanent": true,
    "bvn": "1234567890",
    "tx_ref": "unique_reference"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The security advantage: virtual accounts eliminate payment redirection fraud. When customers pay directly to their assigned account number, there's no chance for attackers to intercept and change payment details. Plus, every payment triggers a webhook notification, giving you real-time visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Combat Invoice Fraud with Integrated Payment Invoicing&lt;/strong&gt;&lt;br&gt;
The fundamental vulnerability of traditional B2B invoicing is the static, insecure PDF attachment. An attacker intercepts the email, uses a PDF editor to change the banking details on the invoice, and forwards it to the accounts payable department. The payment is then sent to the fraudster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Flutterwave Can Help&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://flutterwave.com/ng/invoices" rel="noopener noreferrer"&gt;Flutterwave Invoicing&lt;/a&gt; provides an architectural solution that &lt;em&gt;designs this fraud vector out of the process&lt;/em&gt;. Instead of attaching an insecure, editable file to an email, the Flutterwave system creates an end-to-end, secure loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You create a professional invoice on your Flutterwave dashboard, detailing the services, cost, and due date.&lt;/li&gt;
&lt;li&gt;Flutterwave sends an email to your customer containing a unique, secure link to view and pay this invoice.&lt;/li&gt;
&lt;li&gt;When the customer clicks the "Pay Invoice" button, they are taken &lt;em&gt;directly&lt;/em&gt; to a secure, Flutterwave-hosted payment page.&lt;/li&gt;
&lt;li&gt;This page is already PCI DSS compliant and supports multiple payment methods (card, bank transfer, etc.), all secured by Flutterwave's infrastructure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This system effectively short-circuits the most common and damaging B2B attack vector. It also provides significant operational benefits, such as automated payment tracking, status updates (e.g., "Paid," "Pending"), and automated reminders for overdue invoices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monitor and Alert on Suspicious Activity&lt;/strong&gt;&lt;br&gt;
Fraud prevention requires catching suspicious activity in real time, before the transaction completes. This means implementing fraud detection systems that monitor transactions, flag anomalies, and trigger alerts so your team can act before funds leave your account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Flutterwave Can Help&lt;/strong&gt;&lt;br&gt;
Flutterwave provides key monitoring layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transaction monitoring systems&lt;/strong&gt; analyze payment patterns in real-time. Unusual transaction amounts, rapid successive payments, or payments from unexpected locations trigger automatic alerts.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;24/7 fraud desk&lt;/strong&gt; is staffed with security specialists who review flagged transactions and coordinate with financial institutions when fraud is detected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should supplement Flutterwave's monitoring with your own application-level checks, like implementing velocity checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example velocity check using Redis&lt;/span&gt;
&lt;span class="c1"&gt;// We combine User ID and IP to avoid blocking legitimate corporate networks (NATs)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attemptKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`payment_attempts:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ipAddress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attemptKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attemptKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour window&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Too many payment attempts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;: Don't just log suspicious activity, act on it. Implement automatic blocks for clearly fraudulent patterns and manual review workflows for your finance ops team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Protect Data Throughout the Payment Flow&lt;/strong&gt;&lt;br&gt;
Security means more than fraud prevention; it's about protecting sensitive customer data from the moment a payment is initiated to final processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Flutterwave Can Help&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.flutterwave.com/docs/encryption" rel="noopener noreferrer"&gt;&lt;strong&gt;Encryption&lt;/strong&gt;&lt;/a&gt;: Flutterwave uses advanced encryption protocols for all data transmission. When you send card details to the API, they're encrypted in transit using industry-standard TLS. Stored data is encrypted at rest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KYC and verification&lt;/strong&gt;: Before merchants can process payments through Flutterwave, they undergo thorough &lt;a href="https://flutterwave.com/us/blog/your-guide-to-kyc-at-flutterwave-keeping-things-safe-and-simple" rel="noopener noreferrer"&gt;Know Your Customer (KYC)&lt;/a&gt; verification. This prevents fraudsters from setting up fake merchant accounts. For customers, verification methods like BVN (Bank Verification Number) linking add another security layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access controls&lt;/strong&gt;: Implement role-based access in your application. Not everyone on your finance or ops team needs permission to initiate payments. Your procurement team might create payment requests, but only finance should approve and execute them. Use multi-factor authentication for anyone with payment approval authority.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data minimization&lt;/strong&gt;: Only collect and store the data you actually need. The less sensitive information you hold, the less you can lose in a breach. Use Flutterwave's tokenization to avoid storing card details entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IP whitelisting:&lt;/strong&gt; Configure your Flutterwave dashboard to only accept specific server IP addresses for sensitive operations like payouts. Even if an attacker manages to steal your secret API keys, they cannot move funds because their requests will originate from an unauthorized IP and be blocked immediately.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Building secure B2B payment systems means thinking about security at every step. The best defense combines multiple layers: 3DS2 authentication to verify cardholders, tokenization to protect stored payment data, virtual accounts to prevent payment redirection, and real-time monitoring to catch fraud before funds leave your account. But none of these measures works in isolation; strong authentication won't help if poor access controls let unauthorized users initiate payments in the first place. Build security into every layer of your payment infrastructure.&lt;/p&gt;

&lt;p&gt;Ready to implement these security practices? &lt;a href="https://flutterwave.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Create a Flutterwave account&lt;/strong&gt;&lt;/a&gt; and start building payment systems that protect your business and your customers.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>resources</category>
      <category>security</category>
    </item>
    <item>
      <title>Beyond Black Friday: A Checklist to Keep Your Checkout Stable Through Christmas</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 12 Dec 2025 14:44:48 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/beyond-black-friday-a-checklist-to-keep-your-checkout-stable-through-christmas-115l</link>
      <guid>https://forem.com/flutterwaveeng/beyond-black-friday-a-checklist-to-keep-your-checkout-stable-through-christmas-115l</guid>
      <description>&lt;p&gt;We're deep into peak shopping season. Between now and New Year's Day, your site will face sustained traffic spikes, last-minute Christmas shoppers, and the Boxing Day/New Year sales frenzy that follows.&lt;/p&gt;

&lt;p&gt;This is the nightmare scenario that keeps e-commerce teams up at night during the holiday season. When checkout breaks during peak traffic, you're not just losing sales, you're losing customer trust. Customers who face payment friction won't wait around; they'll compare prices at a competitor's site and complete their purchase there in seconds.&lt;/p&gt;

&lt;p&gt;But don’t worry. Most holiday season payment disasters are preventable, as the problem isn't a lack of solutions; it's a lack of preparation. This holiday season checklist will help you avoid payment failures and boost sales by keeping your checkout running smoothly when traffic spikes.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you'll have a clear action plan to prepare your payment infrastructure using Flutterwave. Each item includes specific implementation steps you can complete before the holiday arrives, along with validation checks to confirm you're truly ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Start: Prerequisites
&lt;/h2&gt;

&lt;p&gt;This checklist assumes you already have Flutterwave integrated into your checkout. If you're new to Flutterwave or haven't integrated yet, start with the &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/getting-started" rel="noopener noreferrer"&gt;Quick Start Guide&lt;/a&gt; to get your basic payment flow working for your existing customers, then come back here to prepare for the holiday season traffic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This implementation uses the Flutterwave v3 API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Your Holiday Season Sales Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Offer Global Payment Methods for Holiday Gifting&lt;/strong&gt;&lt;br&gt;
During the holidays, people aren't just buying for themselves; they are buying gifts for family across borders. A customer in the UK might be buying for family in Kenya, or someone in Lagos might be shopping on a US site. If you only accept local cards, you block these international holiday sales. Flutterwave supports cards, bank transfers, mobile money (M-Pesa, MTN Mobile Money, and more), across multiple countries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See the full list of supported &lt;a href="https://developer.flutterwave.com/v3.0/docs/payment-methods" rel="noopener noreferrer"&gt;payment methods&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Missing payment methods becomes critical during high-traffic periods because customers won’t wait; they’ll find alternatives, abandon their cart, and shop elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit your current payment methods&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Log into your &lt;a href="http://app.flutterwave.com" rel="noopener noreferrer"&gt;Flutterwave dashboard&lt;/a&gt; and review which payment channels you have enabled. Go to “Settings” → “Business preference” → “Payment methods.”&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiofs2a7bkhlxa4ex5qrk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiofs2a7bkhlxa4ex5qrk.png" alt="Flutterwave dashboard payment methods configuration page" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable region-specific methods&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; For Nigerian customers, activate USSD, bank transfer, and QR codes. For East African markets, confirm &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/mobile-money-1" rel="noopener noreferrer"&gt;mobile money&lt;/a&gt; is enabled for Kenya (M-PESA), Uganda, Ghana, Zambia, Rwanda, and Tanzania.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configure your checkout&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; When integrating via API, specify which payment methods to display:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nc"&gt;FlutterwaveCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_PUBLIC_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// stored in a .env&lt;/span&gt;
      &lt;span class="na"&gt;payment_options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card, banktransfer, mobilemoneyghana, ussd, qr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Payment methods are tied to specific currencies; for instance, M-Pesa is only available for KES, so Flutterwave automatically filters unavailable options based on the transaction currency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2. Speed Up Checkout for Repeat Holiday Shoppers&lt;/strong&gt;&lt;br&gt;
Holiday shopping is rarely a one-time event. Customers who have shopped with you before expect a one-click experience. Forcing them to find and re-enter their card details adds friction that will send them to a competitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;br&gt;
Here are some ways you can speed up checkout for returning customers with Flutterwave:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Save payment details for returning customers:&lt;/strong&gt;&lt;br&gt;
Flutterwave offers some approaches to reduce checkout friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/flutterwaveeng/tokenization-vs-encryption-for-payment-data-security-1185"&gt;&lt;strong&gt;Tokenization&lt;/strong&gt;&lt;/a&gt;: Securely save a customer's card details after their first purchase, so they can check out with a single click.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/flutterwaveeng/implementing-card-on-file-for-your-app-a-developers-guide-45e8"&gt;&lt;strong&gt;Card-on-File (CoF)&lt;/strong&gt;&lt;/a&gt;: Store customer cards with enhanced security and compliance for recurring or future payments. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Other ways to speed up checkout:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimize your checkout UI&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Pre-fill customer information when possible and minimize required fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Payment Links for quick checkouts&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Create payment links with QR codes that customers can scan to make payments quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Run Load Tests Before Promotions Go Live&lt;/strong&gt;&lt;br&gt;
Your payment integration might work perfectly under normal load, but collapse when 1,000 customers try to check out simultaneously during the holiday season’s promotions. Load testing reveals bottlenecks before they become disasters, identifying issues like database connection limits, API timeout configurations, or webhook processing delays that only surface under stress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Switch to test mode&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Toggle to the test environment in your Flutterwave dashboard to run tests without processing real transactions.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy10jcgftn7l79e90eu5q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy10jcgftn7l79e90eu5q.png" alt="Test mode toggle" width="410" height="234"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Simulate peak load&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Use load testing tools (JMeter, k6, Artillery) to simulate 10x your normal traffic. Create test scenarios that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process 100+ simultaneous payments&lt;/li&gt;
&lt;li&gt;Mix different payment methods&lt;/li&gt;
&lt;li&gt;Include payment failures and retries&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use test cards for payment s&lt;/strong&gt;&lt;strong&gt;cenario&lt;/strong&gt;&lt;strong&gt;s&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Flutterwave provides test cards to simulate different payment scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Successful payment: Use visa card &lt;code&gt;4187427415564246&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Failed payments: Use card &lt;code&gt;5258585922666506&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Insufficient funds scenarios: Use the appropriate test cards from Flutterwave's test &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/testing#cards" rel="noopener noreferrer"&gt;card documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test webhook handling&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Simulate webhook delays and failures to confirm your system handles them gracefully.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor response times&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Your payment initiation should complete in under two seconds, even at peak load.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;4. Wire Up Webhooks to Handle Asynchronous Holiday Traffic&lt;/strong&gt;&lt;br&gt;
Webhooks are especially useful for asynchronous payment methods like bank transfers, where you won't know when payments are completed unless your payment gateway notifies you. During the holiday season sales, if your webhook endpoint fails, you won't know about successful payments, leaving customers in limbo and support teams scrambling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure webhooks in the dashboard:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to Settings → Webhooks.&lt;/li&gt;
&lt;li&gt;Add your webhook URL (must be publicly accessible).&lt;/li&gt;
&lt;li&gt;Enable "Enable Webhook retries" and "Enable webhook for failed transactions" options.&lt;/li&gt;
&lt;li&gt;Save your configuration.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggr9jube0u85385fg9o6.png" alt="flutterwave’s webhook dashboard" width="800" height="411"&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify webhook signatures&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Always verify the signature using the verif-hash header to confirm requests come from Flutterwave, not attackers sending fake payment confirmations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle retries properly&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; If your webhook endpoint returns an error, Flutterwave retries up to three times with 30-minute intervals between attempts. Make your webhook processing idempotent so these retries don't cause duplicate orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond quickly&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Your webhook URL needs to respond within a certain time limit, or Flutterwave will consider it a failure and retry. Avoid long-running tasks in your webhook endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Learn more about setting up webhooks properly in &lt;a href="https://dev.to/flutterwaveeng/what-are-webhooks-and-how-do-you-implement-them-15j4"&gt;this guide&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;5. Configure Your System for High-Volume Traffic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkge40y822ko8cmnq3gl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkge40y822ko8cmnq3gl.png" alt="trafic surge: normal vs peak load" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Holiday traffic comes in waves. You might see a spike at midnight for a flash sale, and another at noon. If your payment infrastructure can't scale, customers will see timeout errors or stuck loading screens right when they're trying to give you money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configure checkout timeouts&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Set &lt;code&gt;session_duration&lt;/code&gt; to limit completion time for each payment, and configure &lt;code&gt;max_retry&lt;/code&gt; to prevent users from making too many attempts for failed transactions.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nc"&gt;FlutterwaveCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
      &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session_duration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;max_retry_attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale your webhook handlers&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Your webhook processing should be able to handle 100+ webhooks per minute without backing up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use connection pooling&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Configure proper database connection limits to prevent exhaustion during traffic spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up auto-scaling&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; If you're on cloud infrastructure (AWS, GCP, Azure), configure auto-scaling for your payment processing services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Set Refund and Chargeback Playbooks&lt;/strong&gt;&lt;br&gt;
Holiday season sales generates higher-than-normal refund and chargeback volumes due to rushed purchases, shipping delays, and increased fraud attempts. &lt;/p&gt;

&lt;p&gt;Never refund a transaction that's already in the chargeback process. If you do, the customer gets paid twice: once from your refund and once when the chargeback is completed. You need clear playbooks to prevent this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;For refunds:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document your refund approval process (who approves, under what conditions).&lt;/li&gt;
&lt;li&gt;Train your team on how to log a refund via &lt;a href="https://flutterwave.com/ng/support/my-account/refunding-customers" rel="noopener noreferrer"&gt;Dashboard&lt;/a&gt; (Dashboard → Transactions → Refunds) and &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/refunds" rel="noopener noreferrer"&gt;API&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set up email notifications for refund requests.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiazhk86w3m1noetrmc3s.png" alt="Dashboard refund page" width="800" height="344"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For chargebacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check out Flutterwave’s &lt;a href="https://developer.flutterwave.com/v3.0/docs/chargebacks" rel="noopener noreferrer"&gt;chargebacks documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Prepare evidence templates (proof of delivery, service logs, terms of service).&lt;/li&gt;
&lt;li&gt;Assign someone to monitor chargeback notifications.&lt;/li&gt;
&lt;li&gt;Remember: You have 48 hours to respond with evidence.&lt;/li&gt;
&lt;li&gt;Never refund a transaction that's already a chargeback (customer gets paid twice).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;7. Enable Split Payments (For Marketplaces)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsi03halwq4z968qoi2wo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsi03halwq4z968qoi2wo.jpg" alt="Automatic split payment" width="800" height="1063"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suppose you run a marketplace or work with vendors; manually calculating and distributing payments after the holiday season sales is slow and error-prone. Split payments automatically divide incoming payments between your account and vendor accounts, settling funds based on your configuration, which is important when processing thousands of transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;o Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Create subaccounts for vendors:&lt;/strong&gt;&lt;br&gt;
You can set up subaccounts through the dashboard or programmatically via API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Via Dashboard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Dashboard: Click "Subaccounts" →  “Subaccounts” → "Add subaccount"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fae475dsgzdnhvajtzzj0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fae475dsgzdnhvajtzzj0.png" alt="subaccount page" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enter bank details, set split type (percentage or flat), and define split value for default commission rules.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg13py5huhbddpgozts5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg13py5huhbddpgozts5r.png" alt="create subaccount page" width="649" height="824"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Via API&lt;/strong&gt;&lt;br&gt;
For automated vendor onboarding or managing multiple subaccounts programmatically, use the &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/split-payments" rel="noopener noreferrer"&gt;Subaccounts API&lt;/a&gt; to create and configure splits.&lt;/p&gt;

&lt;p&gt;Flutterwave automatically splits the incoming payment &lt;em&gt;at the point of transaction&lt;/em&gt;. The vendor (subaccount) gets their share, and the marketplace (main account) gets its commission, all deposited into the correct accounts instantly and automatically. This automates the entire payout and reconciliation process.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can learn more about split payments and subaccounts in &lt;a href="https://developer.flutterwave.com/v3.0/docs/split-payments" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;8. Create a Payment Monitoring Dashboard&lt;/strong&gt;&lt;br&gt;
You can't fix what you can't see. During the holiday season sales, you need real-time visibility into payment health to catch problems before they become catastrophes. A spike in failed payments or webhook errors needs immediate attention, not discovery after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; &lt;strong&gt;To&lt;/strong&gt; &lt;strong&gt;Solve This&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ith Flutterwave&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set up metrics collection&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Track these key indicators:

&lt;ul&gt;
&lt;li&gt;Payment initiation success rate&lt;/li&gt;
&lt;li&gt;Payment completion rate by method&lt;/li&gt;
&lt;li&gt;Webhook delivery success rate&lt;/li&gt;
&lt;li&gt;Average payment processing time&lt;/li&gt;
&lt;li&gt;Failed payment reasons (by error code)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Flutterwave's dashboard&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Monitor transactions in real-time through the transactions page on your dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up on-call rotation&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Assign team members to monitor sales during peak holiday season hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepare troubleshooting playbooks&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; Document what to do when each alert fires.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This checklist gives you everything you need to prepare, but you have to put in the work to prepare your system ahead of the holiday. Start working through these items today. The time you invest now in proper testing, configuration, and monitoring will pay for itself many times over when your checkout keeps humming while competitors are scrambling to fix broken payments.&lt;/p&gt;

&lt;p&gt;Make this holiday season your biggest revenue day. Your infrastructure is ready, so make sure you are too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need help getting your payment infrastructure ready for the holiday season?&lt;/strong&gt; &lt;a href="https://flutterwave.com" rel="noopener noreferrer"&gt;Sign up for Flutterwave&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>holiday</category>
      <category>flutterwave</category>
      <category>christmassales</category>
    </item>
    <item>
      <title>8 Simple Tips For Testing Payment Gateway Integrations</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 28 Nov 2025 13:20:04 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/8-simple-tips-for-testing-payment-gateway-integrations-586a</link>
      <guid>https://forem.com/flutterwaveeng/8-simple-tips-for-testing-payment-gateway-integrations-586a</guid>
      <description>&lt;p&gt;Your payment integration just went live. Everything worked perfectly in staging. Then, when real users started transacting, they were unsuccessful. Customers can't complete purchases, support tickets flood in, and your business starts losing revenue. This is a situation you don’t want to find yourself in, and you won’t if you test your payment gateway properly.&lt;/p&gt;

&lt;p&gt;Failed payments cost businesses $118.5 billion annually, according to this &lt;a href="https://risk.lexisnexis.com/about-us/press-room/press-release/20210714-true-cost-of-failed-payments" rel="noopener noreferrer"&gt;2020 report&lt;/a&gt;, and most failures could have been caught with proper testing of your payment processor integration. Unlike bugs that frustrate users, payment failures directly cost you money and trust.&lt;/p&gt;

&lt;p&gt;This guide shows you how to test payment integrations properly. You'll learn the payment gateway test cases that matter for cards, bank transfers, and mobile money, plus eight practical tips to catch issues before your customers do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Testing Payment Gateway Integrations Matters
&lt;/h2&gt;

&lt;p&gt;Payment testing isn't like testing other features in your application. When a button stops working in your app, the impact varies; some users might get frustrated, others may just find a workaround. But when payments fail, you lose money and trust. &lt;/p&gt;

&lt;p&gt;Testing payment integrations is different because multiple parties and payment gateway types are involved in every transaction. Your application talks to the gateway, which communicates with issuing banks and card networks. Let's look at the specific scenarios you need to test to avoid these failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Payment Gateway Integration Test Cases
&lt;/h2&gt;

&lt;p&gt;Before diving into testing strategies, let's cover the scenarios every payment integration must handle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction Processing Tests&lt;/strong&gt;&lt;br&gt;
Start with successful transactions: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify transaction IDs are generated and stored correctly &lt;/li&gt;
&lt;li&gt;Check that confirmation emails or SMS notifications are sent &lt;/li&gt;
&lt;li&gt;Test idempotency to prevent the same request from charging twice &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test failures too: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insufficient funds scenarios &lt;/li&gt;
&lt;li&gt;Invalid card details &lt;/li&gt;
&lt;li&gt;Declined transactions &lt;/li&gt;
&lt;li&gt;Gateway downtime responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security Testing&lt;/strong&gt;&lt;br&gt;
Security testing ensures you maintain a secure payment process. Here's what you need to verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify that full card numbers are never logged or stored in your database.&lt;/li&gt;
&lt;li&gt;Confirm CVV codes are never stored anywhere, not even in encrypted form.&lt;/li&gt;
&lt;li&gt;Check that all payment requests use HTTPS.&lt;/li&gt;
&lt;li&gt;Run SQL injection tests on all payment form inputs.&lt;/li&gt;
&lt;li&gt;Test that rate limiting blocks repeated payment attempts with different card numbers.&lt;/li&gt;
&lt;li&gt;Test for parameter tampering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Payment Method-Specific Tests&lt;/strong&gt;&lt;br&gt;
Different payment methods fail in different ways. For example, card payments often fail instantly due to bank declines or fraud checks, while mobile money payments can fail midway through a user's USSD session. Here's what to test for cards and mobile money payments:&lt;br&gt;
Cards (Visa, Mastercard, Amex, Verve):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Card validation using the Luhn algorithm.&lt;/li&gt;
&lt;li&gt;International cards need currency conversion testing.&lt;/li&gt;
&lt;li&gt;Test declined cards, expired cards, and insufficient funds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mobile money (M-Pesa, MTN MoMo, Airtel Money):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can exit USSD sessions mid-flow, leaving payments incomplete.&lt;/li&gt;
&lt;li&gt;Account balance checks before payment prevent failed transactions.&lt;/li&gt;
&lt;li&gt;Test what happens when a user's phone dies during payment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User Experience and Edge Cases&lt;/strong&gt;&lt;br&gt;
Clear error messages matter more in payments than anywhere else, especially when handling card transactions. Avoid generic "Error 500" messages. Look out for button loading states and disable the submit button after the user clicks to prevent double charges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance and Load Testing&lt;/strong&gt;&lt;br&gt;
Payment gateway rate limits will throttle requests if you're not careful. Test what happens at twice your expected load. Graceful degradation matters when systems get overwhelmed.&lt;/p&gt;
&lt;h2&gt;
  
  
  8 Practical Tips For Testing Payment Gateway Integration
&lt;/h2&gt;

&lt;p&gt;Now for practical guidance. Here are eight practical tips you need to know before your payment integration can confidently go live:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 1: Start with a Sandbox&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;est&lt;/strong&gt; &lt;strong&gt;E&lt;/strong&gt;&lt;strong&gt;nvironment&lt;/strong&gt;&lt;br&gt;
Every payment provider offers a test environment where you can simulate possible workflows for your payment needs. For example, Flutterwave provides &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/authentication" rel="noopener noreferrer"&gt;a test mode&lt;/a&gt; that allows you to &lt;strong&gt;experiment, debug, and validate your integration&lt;/strong&gt; without using real money. This sandbox is where you can safely simulate successful and failed transactions, mock different payment methods (like cards, bank transfers, and mobile money), test webhook responses, and verify your error handling, all using your test API keys.&lt;/p&gt;

&lt;p&gt;All you need to do is set up a test environment that mirrors production. Use test API keys stored in &lt;code&gt;.env&lt;/code&gt; files, and never commit them to version control. &lt;/p&gt;

&lt;p&gt;Sandbox environments mirror real-world scenarios in useful ways. Rate limits in test mode let you practice handling throttled requests before they affect customers. Compressed time delays mean a three-day bank transfer completes instantly, so you can test the full flow without waiting. Webhook behavior in the sandbox environment helps you build robust handlers that work regardless of timing variations.&lt;/p&gt;

&lt;p&gt;That said, run a small production test (like a ₦100 transaction with your own card) before full launch. This catches edge cases that only show up with real payment flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 2: Use&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;est&lt;/strong&gt; &lt;strong&gt;C&lt;/strong&gt;&lt;strong&gt;ards&lt;/strong&gt; &lt;strong&gt;S&lt;/strong&gt;&lt;strong&gt;trategically&lt;/strong&gt;&lt;br&gt;
You don’t want to just test with one success card and call it done. This catches happy path bugs but misses most real-world failures. You should build a test card matrix that covers different situations systematically. &lt;/p&gt;

&lt;p&gt;Most payment gateways provide test cards that simulate different scenarios. Here's what your test matrix should look like (using common test card patterns as an example)&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Card Number&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Expected Result&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;4242 4242 4242 4242&lt;/td&gt;
&lt;td&gt;Charge succeeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decline&lt;/td&gt;
&lt;td&gt;4000 0000 0000 0002&lt;/td&gt;
&lt;td&gt;Generic decline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insufficient funds&lt;/td&gt;
&lt;td&gt;4000 0000 0000 9995&lt;/td&gt;
&lt;td&gt;Specific error code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expired card&lt;/td&gt;
&lt;td&gt;4000 0000 0000 0069&lt;/td&gt;
&lt;td&gt;Expired error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVC failure&lt;/td&gt;
&lt;td&gt;4000 0000 0000 0127&lt;/td&gt;
&lt;td&gt;CVC check fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing error&lt;/td&gt;
&lt;td&gt;4000 0000 0000 0119&lt;/td&gt;
&lt;td&gt;Gateway error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Implementation example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Organized test cases&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testCards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4242424242424242&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cvv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12/25&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;decline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4000000000000002&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cvv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12/25&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;insufficientFunds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4000000000000995&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cvv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12/25&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment Processing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handles successful payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testCards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handles declined payment gracefully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testCards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;declined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each test card reveals different failure modes. Test them all to build a payment system that handles real-world scenarios. For Flutterwave integrations, you can use these &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/testing#successful-payments" rel="noopener noreferrer"&gt;test cards&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 3: Test your&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;ebhook&lt;/strong&gt; &lt;strong&gt;I&lt;/strong&gt;&lt;strong&gt;mplementation&lt;/strong&gt; &lt;strong&gt;T&lt;/strong&gt;&lt;strong&gt;horoughly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2ksvapa9jzk5g8gwl87.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb2ksvapa9jzk5g8gwl87.jpg" alt="webhook process" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Webhooks notify you when a payment is confirmed. If they break, you might charge customers without delivering products, mark successful orders as failed, or process payments twice. None of these outcomes are acceptable. &lt;/p&gt;

&lt;p&gt;Use tools like &lt;a href="https://webhook.site/" rel="noopener noreferrer"&gt;webhook.site&lt;/a&gt; or &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; to inspect webhook payloads during development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Expose&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;receive&lt;/span&gt; &lt;span class="nx"&gt;webhooks&lt;/span&gt;
    &lt;span class="nx"&gt;ngrok&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;webhook&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="nx"&gt;becomes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//&amp;lt;abc123&amp;gt;.ngrok.io/webhooks/payment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you see exactly what data the gateway sends and debug issues before production. You can also write tests that verify webhook handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook Handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifies webhook signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeWebhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFakeWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeWebhook&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Reject invalid signatures&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handles duplicate webhooks idempotently&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createValidWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;txnId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TXN123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// First delivery&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Duplicate&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TXN123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Only processed once&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test what happens when your server is down. Verify how your retry and recovery mechanism works. Check webhook logs in your payment gateway dashboard. Implement manual webhook replay for critical transactions that failed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you haven't set up webhooks yet, check out &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/getting-started" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; on implementing Flutterwave webhooks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Tip 4: Implement&lt;/strong&gt; &lt;strong&gt;L&lt;/strong&gt;&lt;strong&gt;ogging that&lt;/strong&gt; &lt;strong&gt;P&lt;/strong&gt;&lt;strong&gt;rotects&lt;/strong&gt; &lt;strong&gt;S&lt;/strong&gt;&lt;strong&gt;ensitive&lt;/strong&gt; &lt;strong&gt;D&lt;/strong&gt;&lt;strong&gt;ata&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokxigbqhsg1ihq4ecr31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fokxigbqhsg1ihq4ecr31.png" alt="payment logging do’s and don’ts" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need logs to debug production issues. Every payment transaction should include key metadata that helps you trace issues without exposing sensitive data. Here's what to log: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transaction ID&lt;/li&gt;
&lt;li&gt;Amount and currency &lt;/li&gt;
&lt;li&gt;Payment method type &lt;/li&gt;
&lt;li&gt;Last four digits of the card only &lt;/li&gt;
&lt;li&gt;Transaction status and timestamps &lt;/li&gt;
&lt;li&gt;User ID
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// GOOD: Log transaction metadata&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment initiated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ORD-123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NGN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;last4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4242&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Last 4 digits only&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// BAD: Never log this&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment attempt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4242424242424242&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// PCI violation&lt;/span&gt;
      &lt;span class="na"&gt;cvv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// NEVER store CVV&lt;/span&gt;
      &lt;span class="na"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;John Doe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;john@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://dev.to/flutterwaveeng/passing-pci-checks-starts-earlier-than-you-think-36a"&gt;PCI DSS&lt;/a&gt; compliance forbids logging full card numbers or CVV codes under any circumstances, even in encrypted form. Violating this can result in hefty fines and losing your ability to process payments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a customer reports a failed payment, you should be able to search logs by order ID or user email, see the complete transaction timeline, identify the exact failure point, and determine if the issue is client-side, your server, or the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 5: Test&lt;/strong&gt; &lt;strong&gt;C&lt;/strong&gt;&lt;strong&gt;urrency and&lt;/strong&gt; &lt;strong&gt;A&lt;/strong&gt;&lt;strong&gt;mount&lt;/strong&gt; &lt;strong&gt;H&lt;/strong&gt;&lt;strong&gt;andling with&lt;/strong&gt; &lt;strong&gt;P&lt;/strong&gt;&lt;strong&gt;recision&lt;/strong&gt;&lt;br&gt;
Financial calculations using floating-point arithmetic create disasters waiting to happen. JavaScript's &lt;code&gt;0.1 + 0.2 === 0.3&lt;/code&gt; returns false, which should terrify anyone handling money.&lt;br&gt;
Use integer cents like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Store amounts as integer cents/kobo&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ₦9.99 as 999 kobo&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1998 kobo = ₦19.98&lt;/span&gt;

    &lt;span class="c1"&gt;// Or use a decimal library&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decimal.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;9.99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Accurate calculation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test &lt;a href="https://dev.to/flutterwaveeng/is-your-multi-currency-payments-setup-heading-for-disaster-3e4e"&gt;different currencies&lt;/a&gt;, as currency-specific rules matter because, for example, the Japanese Yen has no decimals, while most currencies use two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Test different currencies&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testCases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NGN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expectedDisplay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;₦1,000.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UGX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expectedDisplay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USh 1,000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// No decimal&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JPY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expectedDisplay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;¥1,000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// No decimal&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nx"&gt;testCases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedDisplay&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedDisplay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero amount transactions should fail validation, and negative amounts need different handling for refunds versus charges. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 6: Create a&lt;/strong&gt; &lt;strong&gt;P&lt;/strong&gt;&lt;strong&gt;re-&lt;/strong&gt;&lt;strong&gt;P&lt;/strong&gt;&lt;strong&gt;roduction&lt;/strong&gt; &lt;strong&gt;C&lt;/strong&gt;&lt;strong&gt;hecklist and&lt;/strong&gt; &lt;strong&gt;A&lt;/strong&gt;&lt;strong&gt;ctually&lt;/strong&gt; &lt;strong&gt;U&lt;/strong&gt;&lt;strong&gt;se&lt;/strong&gt; &lt;strong&gt;I&lt;/strong&gt;&lt;strong&gt;t&lt;/strong&gt;&lt;br&gt;
Before going live, run through this checklist:&lt;br&gt;
Configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production API keys are in place, and test keys are removed.&lt;/li&gt;
&lt;li&gt;Webhook URL points to the production endpoint.&lt;/li&gt;
&lt;li&gt;The database connection pool is sized for the expected load.&lt;/li&gt;
&lt;li&gt;Error monitoring is configured (&lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;, &lt;a href="https://rollbar.com/" rel="noopener noreferrer"&gt;Rollbar&lt;/a&gt;, or similar).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Communications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment success emails work correctly.&lt;/li&gt;
&lt;li&gt;Payment failure emails are sent with clear next steps.&lt;/li&gt;
&lt;li&gt;SMS notifications are configured and tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All test cards produce expected results.&lt;/li&gt;
&lt;li&gt;Webhook handler processes all event types.&lt;/li&gt;
&lt;li&gt;Failed payment retry logic works as expected.&lt;/li&gt;
&lt;li&gt;Transaction logs exclude sensitive data.&lt;/li&gt;
&lt;li&gt;Mobile payment flows tested on real devices.&lt;/li&gt;
&lt;li&gt;Load tested at twice your expected traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use these tips to adapt your own checklist and make it a GitHub issue template or Notion document that must be completed before each deployment. Checklists prevent mistakes when you're tired or rushing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 7: Automate&lt;/strong&gt; &lt;strong&gt;W&lt;/strong&gt;&lt;strong&gt;hat&lt;/strong&gt; &lt;strong&gt;Y&lt;/strong&gt;&lt;strong&gt;ou&lt;/strong&gt; &lt;strong&gt;C&lt;/strong&gt;&lt;strong&gt;an,&lt;/strong&gt; &lt;strong&gt;b&lt;/strong&gt;&lt;strong&gt;ut&lt;/strong&gt; &lt;strong&gt;K&lt;/strong&gt;&lt;strong&gt;eep&lt;/strong&gt; &lt;strong&gt;C&lt;/strong&gt;&lt;strong&gt;ritical&lt;/strong&gt; &lt;strong&gt;F&lt;/strong&gt;&lt;strong&gt;lows&lt;/strong&gt; &lt;strong&gt;M&lt;/strong&gt;&lt;strong&gt;anual&lt;/strong&gt;&lt;br&gt;
Run regression tests on your core payment flows. Regression test will catch breaking changes before customers do. Set up API health checks to monitor gateway availability, and schedule load tests to run regularly.&lt;/p&gt;

&lt;p&gt;Here are examples of critical flows and scenarios that should be kept manual to catch issues automation might miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;End-to-end flows on real mobile devices.&lt;/li&gt;
&lt;li&gt;Cross-browser testing, especially Safari, catches rendering and JavaScript quirks.&lt;/li&gt;
&lt;li&gt;Edge cases discovered in production need manual verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Follow the 80/20 rule. Automate 80% of repetitive tests, but allocate 20% of testing time to exploratory, manual testing where you try to break things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip 8:&lt;/strong&gt; &lt;strong&gt;Treat Your First Launch Week as the Final Test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4fj98byz8v2mv9y0al3p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4fj98byz8v2mv9y0al3p.png" alt="payment gateway monitoring dashboard" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set up real-time monitoring and set up alerts for these critical thresholds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment success rate drops below 95%&lt;/li&gt;
&lt;li&gt;Gateway API response time exceeds 10 seconds&lt;/li&gt;
&lt;li&gt;More than five webhook failures within 10 minutes&lt;/li&gt;
&lt;li&gt;Any PCI DSS security violations detected&lt;/li&gt;
&lt;li&gt;Database connection pool reaches 80% capacity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can build a single screen showing transactions per minute, success versus failure rate, average payment processing time, top error messages, and gateway uptime status.&lt;/p&gt;

&lt;p&gt;During the first week after launch, check your dashboard frequently. Review all failed transactions daily. Read customer support tickets about payments. Compare metrics with expectations. Fix issues immediately; don't wait for the next sprint.&lt;/p&gt;

&lt;p&gt;Your production debugging workflow should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lsla3509evporrv40r7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lsla3509evporrv40r7.png" alt="payment failure debugging" width="800" height="996"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This workflow helps you respond quickly when customers report issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;This guide walked you through testing online payments and payment gateway integrations properly. You now understand what makes payment testing different, the core test cases for cards, mobile money, and bank transfers, plus eight practical tips to catch issues before your customers do.&lt;/p&gt;

&lt;p&gt;The best payment integration is one your customers never notice. It just works. Every time. Testing makes that possible.&lt;/p&gt;

&lt;p&gt;Ready to build reliable payment integrations? Explore Flutterwave's &lt;a href="https://developer.flutterwave.com/v3.0/docs/authentication" rel="noopener noreferrer"&gt;test mode&lt;/a&gt; for a complete test environment. Also, check out &lt;a href="https://developer.flutterwave.com/docs" rel="noopener noreferrer"&gt;Flutterwave Developer Docs&lt;/a&gt; for more information on getting started.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>payment</category>
      <category>testing</category>
      <category>flutterwave</category>
    </item>
    <item>
      <title>What Are Webhooks, and How Do You Implement Them?</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 07 Nov 2025 11:29:01 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/what-are-webhooks-and-how-do-you-implement-them-15j4</link>
      <guid>https://forem.com/flutterwaveeng/what-are-webhooks-and-how-do-you-implement-them-15j4</guid>
      <description>&lt;p&gt;Imagine a customer just completed a payment at your online store. Now your server needs to know if the transaction went through, so it checks every few seconds, repeatedly asking: 'Is the payment done yet?' This approach is called &lt;strong&gt;&lt;em&gt;polling&lt;/em&gt;&lt;/strong&gt;, and it's wasteful. Your server keeps asking for updates that aren't there yet, burning through resources while missing real-time events.&lt;/p&gt;

&lt;p&gt;Webhooks flip this around. Instead of your server constantly asking "Is the payment done yet?", your payment provider notifies your server the instant the payment goes through.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn what webhooks are, how they work, and most importantly, how to implement them with Flutterwave to build better payment experiences for your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Webhooks?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Webhooks&lt;/strong&gt; are automated messages sent from one application to another when a certain event occurs. In the context of Flutterwave, webhooks are HTTP POST requests that notify your application about payment events in real time.&lt;/p&gt;

&lt;p&gt;Here's a simple way to think about it: Webhooks are like push notifications for your server. If you’re waiting to receive a text message, you don't have to keep opening the messaging app to check for new messages; your phone alerts you immediately. Webhooks work the same way for your application.&lt;/p&gt;

&lt;p&gt;Instead of your server constantly polling Flutterwave for updates about a transaction, which is slow and inefficient, Flutterwave automatically sends you a notification the moment a new event happens.&lt;/p&gt;

&lt;p&gt;In the intro to this guide, we mentioned polling, which is referred to as the Pull Model. Let’s understand the push and pull paradigm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Push vs. Pull Paradigm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidg1dec3swkjmxl4xqb7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidg1dec3swkjmxl4xqb7.jpg" alt="polling vs webhooks" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Webhooks use the "push" model: the server sends data to your application when events occur, while traditional polling uses the "pull" model: your application repeatedly asks the server for updates. Here's how they compare: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Webhooks (Push Model)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;API Polling (Pull Model)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Flow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server pushes data to client when an event occurs.&lt;/td&gt;
&lt;td&gt;The client repeatedly makes requests to retrieve data from the server.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High. Communication only happens when there's new data.&lt;/td&gt;
&lt;td&gt;Low. Wastes resources on requests that yield no new data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Near real-time. Data is sent instantly.&lt;/td&gt;
&lt;td&gt;Delayed. Depends on the polling interval.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server Load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low. Load is proportional to event frequency.&lt;/td&gt;
&lt;td&gt;High. Constant load from all clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;More complex initially (requires a public endpoint).&lt;/td&gt;
&lt;td&gt;Simpler to implement initially.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now you know the difference between webhooks and API polling, let’s look at how webhooks work and how to implement them with Flutterwave in the next sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do Webhooks Work?
&lt;/h2&gt;

&lt;p&gt;Understanding how webhooks work helps you implement them correctly. Here's the complete flow of a webhook in action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Trigger:&lt;/strong&gt; The process begins when a specific event occurs in the webhook provider's system. This could be a customer successfully authorizing a payment, a bank transfer failing, or a subscription being renewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload Generation:&lt;/strong&gt; Upon triggering, the provider's system generates a JSON object, known as the payload. This payload contains detailed information about the event that just occurred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The HTTP POST Request:&lt;/strong&gt; The provider then sends this JSON payload in the body of an HTTP POST request to the webhook URL you have configured. This URL must be a publicly accessible endpoint on your server capable of receiving POST requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receipt and Processing:&lt;/strong&gt; Your application's server, often called a "webhook listener," receives this incoming HTTP request. The first and most important step for your listener is to verify the authenticity of the request to ensure it genuinely came from the legitimate provider. (With Flutterwave, this verification is done using the &lt;a href="https://flutterwave.com/zm/support/integrations/what-is-a-secret-hash" rel="noopener noreferrer"&gt;verif-hash header&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Acknowledgment Handshake:&lt;/strong&gt; To confirm that it has successfully received the webhook, your listener must respond with a &lt;code&gt;200 OK&lt;/code&gt; HTTP status code. This response signals to the provider that the delivery was successful, and no further action is needed for that event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry Mechanism:&lt;/strong&gt; If your endpoint fails to return a &lt;code&gt;200 OK&lt;/code&gt; status code, for example, if it returns a &lt;code&gt;4xx&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt; error, or if the request times out, most providers will consider the delivery to have failed and attempt to resend the webhook. (Flutterwave, for instance, will retry three times with 30-minute intervals between each attempt if you have retries enabled in your dashboard.)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How To Implement Webhooks with Flutterwave
&lt;/h2&gt;

&lt;p&gt;Now let's get into the practical part: implementing webhooks in your application. In this guide, we'll use Express.js and the Flutterwave v3 API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Configure Your Flutterwave Dashboard&lt;/strong&gt;&lt;br&gt;
Before you can receive webhooks, you must specify the webhook endpoint you want Flutterwave to send an update to. This is done through your dashboard.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to your &lt;a href="https://dashboard.flutterwave.com" rel="noopener noreferrer"&gt;Flutterwave dashboard&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Webhooks&lt;/strong&gt;&lt;strong&gt;.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your webhook URL in the &lt;strong&gt;Webhook URL&lt;/strong&gt; field (e.g., &lt;code&gt;https://yourdomain.com/webhooks/flutterwave&lt;/code&gt;). For local development, we will use a temporary URL from a tool called &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;, which we'll cover in step 5.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Secret Hash&lt;/strong&gt; field, enter a long, random, and unpredictable string. Never store this in your code or configuration files; always use environment variables.&lt;/li&gt;
&lt;li&gt;Check the boxes for the webhook options:

&lt;ul&gt;
&lt;li&gt;Receive webhook response in JSON format&lt;/li&gt;
&lt;li&gt;Enable webhook retries&lt;/li&gt;
&lt;li&gt;Enable webhook for failed transactions&lt;/li&gt;
&lt;li&gt;Enable V3 webhooks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;&lt;strong&gt;.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the time you’re done, your webhooks settings should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xvlw8f7vazqnvv5a34a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5xvlw8f7vazqnvv5a34a.png" alt="flutterwave’s webhook dashboard" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Set Up Your Webhook Endpoint&lt;/strong&gt;&lt;br&gt;
Here, we will build our webhook endpoint using Node.js and &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express.js&lt;/a&gt;. This endpoint allows one app (Flutterwave) to communicate with your web app in real time. First, we need to set up our project and install the necessary dependencies: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;express&lt;/code&gt; for the web server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;body-parser&lt;/code&gt; to handle incoming request bodies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crypto&lt;/code&gt; for signature verification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt; to manage environment variables
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="nb"&gt;mkdir &lt;/span&gt;flutterwave-webhook-handler
    &lt;span class="nb"&gt;cd &lt;/span&gt;flutterwave-webhook-handler
    npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
    npm &lt;span class="nb"&gt;install &lt;/span&gt;express body-parser crypto dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file in your project's root directory to store your secret hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    &lt;span class="nv"&gt;FLW_SECRET_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_super_secret_hash_from_the_dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create an endpoint in your Express application to receive webhooks from Flutterwave. This endpoint needs to capture the raw request body because you'll need it to verify the webhook signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// in app.js&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Middleware to capture raw body for signature verification&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="c1"&gt;// Webhook endpoint&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/flutterwave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// We'll add verification and processing logic here&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Always respond with 200 quickly&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error processing webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code above, a few things are happening that you should take note of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use &lt;code&gt;express.raw({ type: 'application/json' })&lt;/code&gt; for our webhook route. This is important because we need the raw, un-parsed request body as a buffer to compute the HMAC hash correctly. Standard JSON middleware like &lt;code&gt;express.json()&lt;/code&gt; would parse the body, altering it and causing the signature verification to fail.&lt;/li&gt;
&lt;li&gt;If verification fails, it immediately responds with a &lt;code&gt;500&lt;/code&gt; and stops processing.&lt;/li&gt;
&lt;li&gt;After successful verification, it sends a &lt;code&gt;200 OK&lt;/code&gt; response. &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Any time-consuming business logic should be handed off to a background process or queue after this response is sent to avoid timeouts.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Verify Webhook Signatures&lt;/strong&gt;&lt;br&gt;
Anyone can send a POST request to your webhook URL, so you need to verify that incoming webhooks are actually from Flutterwave.&lt;/p&gt;

&lt;p&gt;Flutterwave signs every webhook with your secret hash and includes the signature in the &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/webhooks#verifying-webhook-signatures" rel="noopener noreferrer"&gt;verif-hash&lt;/a&gt; &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/webhooks#verifying-webhook-signatures" rel="noopener noreferrer"&gt;header&lt;/a&gt;. Here's how to verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Create a hash of the payload using your secret hash&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Compare with the signature from Flutterwave&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/flutterwave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verif-hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FLW_SECRET_HASH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Verify the webhook is from Flutterwave&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretHash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid webhook signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Webhook is verified, safe to process&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webhookData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Process the webhook data&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook processed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error processing webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Process Webhook Events&lt;/strong&gt;&lt;br&gt;
Once verified, you can process the webhook based on the event type. Each specific event triggers different actions in your application. A successful payment might send a confirmation email, while a failed transfer might notify your support team. Flutterwave sends different events for different actions. Here's how to handle the most common ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;webhookData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleSuccessfulCharge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transfer.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleTransferCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unhandled webhook type: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSuccessfulCharge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tx_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Only process successful transactions&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Payment &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not successful: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Check if you've already processed this webhook (idempotency)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkIfProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; already processed`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Update your database&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updatePaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Send confirmation email to customer&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendConfirmationEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Mark webhook as processed&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markWebhookProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Payment &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; processed successfully`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleTransferCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUCCESSFUL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateTransferStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transfer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; completed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateTransferStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transfer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Test Your Webhooks&lt;/strong&gt;&lt;br&gt;
Since your development machine is not on the public internet, external web services like Flutterwave cannot reach it directly. &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; is a tool that solves this by creating a secure public URL that tunnels traffic directly to a port on your local machine.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Ngrok:&lt;/strong&gt; Download and install &lt;code&gt;ngrok&lt;/code&gt; from the &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;official website&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate:&lt;/strong&gt; Connect your ngrok agent to your account by running the command provided in your ngrok dashboard (this is usually a one-time setup):
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok config add-authtoken &amp;lt;YOUR_NGROK_AUTHTOKEN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;3. Start Your Local Server:&lt;/strong&gt; Run your Node.js application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server is now listening on port 3000.&lt;br&gt;
&lt;strong&gt;4. Start the&lt;/strong&gt; &lt;strong&gt;n&lt;/strong&gt;&lt;strong&gt;grok Tunnel:&lt;/strong&gt; In a new terminal window, tell ngrok to forward public traffic to your local port 3000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Get Your Public URL:&lt;/strong&gt; ngrok will display a screen with a public "Forwarding" URL that looks something like &lt;code&gt;https://random-string.ngrok-free.app&lt;/code&gt;. This is your temporary public webhook URL.&lt;br&gt;
&lt;strong&gt;6. Update Flutterwave Dashboard:&lt;/strong&gt; Copy the HTTPS version of the ngrok URL and append your webhook route (e.g., &lt;code&gt;https://random-string.ngrok-free.app/webhooks/flutterwave&lt;/code&gt;). Paste this full URL into the &lt;strong&gt;Webhook URL&lt;/strong&gt; field in your Flutterwave dashboard's Test Mode settings.  &lt;/p&gt;

&lt;p&gt;You can perform a test transaction with the Flutterwave &lt;a href="https://developer.flutterwave.com/v3.0.0/reference/charge-via-bank-transfer" rel="noopener noreferrer"&gt;bank transfer endpoint&lt;/a&gt;. Just make sure to use your test mode secret key as the authorization header. You can get your secret key at &lt;strong&gt;Settings → API key&lt;/strong&gt; in your dashboard.&lt;/p&gt;

&lt;p&gt;After your test transaction, look at your terminal, and you can see the post request made by Flutterwave to the webhook handler you created.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpekgttuzhxwf9ub99xy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpekgttuzhxwf9ub99xy.png" alt="webhook cli output" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also open ngrok's web interface (at &lt;code&gt;http://localhost:4040&lt;/code&gt;) to inspect the full details of every request and response, including headers and body. You can even replay requests with a single click, which is invaluable for debugging your handler logic without having to perform a new transaction every time.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgk6ux4nb993p7sixl4ny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgk6ux4nb993p7sixl4ny.png" alt="ngrok web interface" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have now learned how to create a webhook handler and test a webhook from Flutterwave, but you still need to follow some best practices to make sure your webhook is secure. Let’s look at some best practices next.&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Practices for Webhook Implementation
&lt;/h2&gt;

&lt;p&gt;Following these best practices will help you build a reliable, secure webhook implementation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Always Use HTTPS&lt;/strong&gt;&lt;br&gt;
Never use HTTP for webhook endpoints. Flutterwave requires HTTPS, and for good reason. It encrypts the data in transit, protecting sensitive payment information from being intercepted. If you're testing locally, tools like ngrok automatically provide HTTPS URLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Verify Every Webhook Signature&lt;/strong&gt;&lt;br&gt;
Never trust a webhook just because it arrived at your endpoint. Always verify the &lt;code&gt;verif-hash&lt;/code&gt; header using your secret hash. This protects you from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attackers sending fake payment confirmations&lt;/li&gt;
&lt;li&gt;Replay attacks where someone tries to resend captured webhooks&lt;/li&gt;
&lt;li&gt;Unauthorized access to your webhook endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without verification, anyone could send fake "payment successful" webhooks to your server. You can read more on securing webhooks in our guide on &lt;a href="https://dev.to/flutterwaveeng/do-you-really-handle-webhooks-securely-1hah"&gt;webhook security.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Respond Quickly (Within 60 Seconds)&lt;/strong&gt;&lt;br&gt;
Flutterwave times out webhook requests after 60 seconds. If your server takes longer to respond, Flutterwave considers it a failure and retries the webhook.&lt;/p&gt;

&lt;p&gt;Respond immediately with a 200 status, then process any heavy logic asynchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/flutterwave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Verify signature&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verif-hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FLW_SECRET_HASH&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Acknowledge receipt immediately&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Process webhook asynchronously (don't await here)&lt;/span&gt;
      &lt;span class="nf"&gt;processWebhookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Background processing error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Make Your Endpoint Idempotent&lt;/strong&gt;&lt;br&gt;
Webhooks can arrive multiple times for the same event due to network issues or retries. Your endpoint should handle &lt;a href="https://developer.flutterwave.com/docs/webhooks#be-idempotent" rel="noopener noreferrer"&gt;duplicate webhooks&lt;/a&gt; gracefully without creating duplicate orders or charging customers twice.&lt;/p&gt;

&lt;p&gt;The best way to handle this is by tracking which webhooks you've already processed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Simple in-memory store (use a database in production)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processedWebhooks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkIfProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;processedWebhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markWebhookProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;processedWebooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Also save to your database for persistence&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveProcessedWebhookToDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each webhook from Flutterwave includes a unique &lt;code&gt;id&lt;/code&gt; field. Store this ID when you process a webhook, and check it before processing future webhooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Handle Retries Properly&lt;/strong&gt;&lt;br&gt;
If your webhook endpoint returns an error status code (anything other than 2xx), Flutterwave will retry sending the webhook up to three times with 30-minute intervals between attempts. This is helpful for temporary failures (your server was restarting, database connection issue, etc.), but you need to make sure your webhook processing is idempotent so these retries don't cause problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Webhooks are the backbone of modern payment integrations. Instead of constantly polling your payment provider's API, webhooks let you receive notifications and real-time data instantly when payment events happen. Webhooks let Flutterwave notify your application almost instantly when payment events happen. No polling, no delays, just real-time updates to your server.&lt;/p&gt;

&lt;p&gt;One of the keys to great payment experiences is keeping your customers informed every step of the way. Webhooks make that possible.&lt;/p&gt;

&lt;p&gt;Ready to implement webhooks in your application? Check out Flutterwave's &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/webhooks" rel="noopener noreferrer"&gt;webhook documentation&lt;/a&gt; for detailed API references.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>fintech</category>
      <category>async</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>How To Set Up QR Code Payments in Your App</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 17 Oct 2025 14:39:15 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/how-to-set-up-qr-code-payments-in-your-app-5bja</link>
      <guid>https://forem.com/flutterwaveeng/how-to-set-up-qr-code-payments-in-your-app-5bja</guid>
      <description>&lt;p&gt;You’re at your favorite local restaurant, ready to pay for your meal. Instead of reaching for your cash or cards, you pull out your phone, scan a &lt;a href="https://en.wikipedia.org/wiki/QR_code" rel="noopener noreferrer"&gt;quick-response&lt;/a&gt; (QR) code on the table, and your contactless payment is complete in seconds. QR code-based payments are a massive global phenomenon, with millions of successful transactions being processed daily for various goods.&lt;/p&gt;

&lt;p&gt;If you're a developer building mobile applications that handle payments, QR codes offer the opportunity for a faster and simplified payment process for your customers. No more complex checkout flows. QR codes reduce complex checkout flows and help minimize cart abandonment. While customers still need to authenticate their payments (via PIN, OTP, or biometrics), the process is significantly faster than traditional payment methods.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you'll know exactly how QR code payments work, how to implement them using &lt;a href="https://developer.flutterwave.com/" rel="noopener noreferrer"&gt;Flutterwave's APIs&lt;/a&gt;, and the security practices that'll keep your users' transactions safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do QR Code Payments Work?
&lt;/h2&gt;

&lt;p&gt;Before we jump into implementation, let's break down what happens when someone pays with a QR code. Understanding this flow will help you build better payment experiences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsxqbjmt8cq9p27m9zko.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsxqbjmt8cq9p27m9zko.jpg" alt="QR Payment Flow" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the step-by-step process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;QR Code Generation:&lt;/strong&gt; Your app creates a unique QR code containing payment details like amount, merchant info, and transaction reference. Think of it as encoding all the payment information into a visual format that any smartphone camera can read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer Scans the Code:&lt;/strong&gt; The customer opens their banking app, payment app, or even just their phone's camera and points it at your QR code. Most modern smartphones can read QR codes natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment Information Extraction:&lt;/strong&gt; Once scanned, the QR code reveals the embedded payment data. The customer's app now knows exactly what they're paying for, how much, and where the money should go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment Authorization:&lt;/strong&gt; The customer reviews the transaction details and confirms the payment using their preferred method (mobile banking, digital wallet, or payment app).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Processing:&lt;/strong&gt; The payment flows through the banking network or payment processor. This happens in the background while your app waits for confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmation and Completion:&lt;/strong&gt; Both you and the customer receive real-time notifications about the payment status. Your app gets webhook notifications to update the transaction status automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The beauty of QR payments lies in their simplicity. Customers can complete QR code transactions in just two clicks, making them perfect for everything from retail stores to mobile apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different QR Payment Methods
&lt;/h2&gt;

&lt;p&gt;Before you set up QR payments in your app, you need to understand the different types of QR payments. Choosing the right model affects both your user experience and technical approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static QR Codes&lt;/strong&gt;&lt;br&gt;
Static QR codes are reusable and don't contain a specific amount. Think of them like a digital &lt;em&gt;"pay here"&lt;/em&gt; sign. They work by allowing the customer to scan your QR code, then enter the amount they want to pay. They’re perfect for scenarios where payment amounts vary. You can use them in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restaurant table payments where customers enter the total themselves&lt;/li&gt;
&lt;li&gt;Donation boxes or tip jars&lt;/li&gt;
&lt;li&gt;Service businesses with variable pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dynamic QR Codes&lt;/strong&gt;&lt;br&gt;
Dynamic QR codes are generated for each transaction and contain specific payment details, including the exact amount. Your app generates a new QR code for each transaction, containing all the payment details. The customer just scans and confirms. No amount entry needed. You can use them in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;E-commerce checkouts&lt;/li&gt;
&lt;li&gt;Bill payments&lt;/li&gt;
&lt;li&gt;Any scenario where you know the exact amount upfront&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyb85qh9wccfnjetdvq8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyb85qh9wccfnjetdvq8.jpg" alt="Different QR payment models" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merchant-Presented vs&lt;/strong&gt;&lt;strong&gt;.&lt;/strong&gt; &lt;strong&gt;Customer-Presented QR Payment Modes&lt;/strong&gt;&lt;br&gt;
Both static and dynamic QR payment models can be in merchant-presented mode&lt;br&gt;
 or customer-presented mode.&lt;br&gt;
&lt;strong&gt;Merchant-Presented Mode&lt;/strong&gt;&lt;br&gt;
In merchant-presented mode, your app displays the QR code, and the customer scans it with their banking app&lt;br&gt;
&lt;strong&gt;Customer-Presented Mode&lt;/strong&gt;&lt;br&gt;
In customer-presented mode, the customer displays a QR code from their banking app, and you scan their code with your app. This is mostly common in in-person service businesses.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For most mobile app implementations, you'll use &lt;strong&gt;dynamic QR codes in merchant-presented mode,&lt;/strong&gt; which is what we will be implementing with Flutterwave.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  How To Set Up a QR Code for Payment with Flutterwave
&lt;/h2&gt;

&lt;p&gt;Let's build a QR payment system. You'll create dynamic QR codes that customers can scan to pay instantly, handle real-time payment confirmations, and add proper security measures to protect transactions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this guide, we‘ll use Flutterwave's &lt;a href="https://developer.flutterwave.com/v3.0.0/reference" rel="noopener noreferrer"&gt;v3 API&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;br&gt;
Before you start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Flutterwave &lt;a href="https://dashboard.flutterwave.com/signup" rel="noopener noreferrer"&gt;account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Your API keys from the Flutterwave dashboard
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffs620h41ltcf1cwzz6ky.png" alt="API Keys" width="800" height="458"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Initialize the Payment Request&lt;/strong&gt;&lt;br&gt;
First, let's create a QR code payment request using Flutterwave's API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initializeQRPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;tx_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`qr-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Unique transaction reference&lt;/span&gt;
        &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NGN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fullname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;is_nqr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="na"&gt;redirect_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-app.com/payment-callback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.flutterwave.com/v3/charges?type=qr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FLW_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QR payment initialization failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Display the QR Code to Your Customer&lt;/strong&gt;&lt;br&gt;
Once you get a successful response, Flutterwave returns a base64-encoded QR code image. Here's how to display it in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;displayQRCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qrImageData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// For web applications&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qrImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qr-code-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;qrImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qrImageData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// For mobile apps, you'll handle this differently&lt;/span&gt;
      &lt;span class="c1"&gt;// React Native example:&lt;/span&gt;
      &lt;span class="c1"&gt;// &amp;lt;Image source={{uri: `data:image/png;base64,${qrImageData}`}} /&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Usage example&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processQRPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderDetails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initializeQRPayment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderDetails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Display the QR code to the customer&lt;/span&gt;
          &lt;span class="nf"&gt;displayQRCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qr_image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

          &lt;span class="c1"&gt;// Store transaction ID for verification&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transactionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paymentResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
          &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currentTransactionId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Handle error appropriately&lt;/span&gt;
        &lt;span class="nf"&gt;showErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unable to generate QR code. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Handle Payment Verification&lt;/strong&gt;&lt;br&gt;
QR payments occur outside your app, so you need to know when customers complete their payments and verify the transactions. Here's how to handle verification:&lt;/p&gt;

&lt;p&gt;First, let's understand what you get back from &lt;strong&gt;&lt;em&gt;Step 1&lt;/em&gt;&lt;/strong&gt;. When you create a QR payment, Flutterwave returns something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Response from initializeQRPayment&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Charge initiated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8217804&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// This is your transaction ID&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tx_ref&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qr-1642123456789&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flw_ref&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FLWTK43726MCK1732546764469&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;charged_amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app_fee&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NGN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nibss-qr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2539403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Billy Butcher&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phone_number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;080000000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qr_image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:image/jpeg;base64,iVBORw0KGgoAAAANS...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// QR code is here!&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;validate_instructions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The QR code provided is for demonstration purposes only.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have two ways to know when the payment completes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Webhooks (Recommended)&lt;/strong&gt;&lt;br&gt;
Set up a webhook endpoint to get instant notifications. This is the best approach for user experience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Webhook endpoint - gets called immediately when payment completes&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook/flutterwave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FLW_SECRET_HASH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verif-hash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

      &lt;span class="c1"&gt;// Always verify webhook signatures&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;secretHash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Always verify server-side even after webhook&lt;/span&gt;
        &lt;span class="nf"&gt;verifyTransactionServerSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Manual Verification (Fallback)&lt;/strong&gt;&lt;br&gt;
Sometimes you need to check payment status manually - maybe the webhook failed or the customer claims they paid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Verify using the transaction ID from Step 1 response&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifyTransaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.flutterwave.com/v3/transactions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/verify`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FLW_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Payment verified - process the order&lt;/span&gt;
          &lt;span class="nf"&gt;processSuccessfulPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verification failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;


    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkPaymentStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isVerified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isVerified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment successful!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment still pending...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Always verify transactions server-side, even after receiving webhooks. Never trust payment status from your frontend alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  QR Payments Security Best Practices
&lt;/h2&gt;

&lt;p&gt;Security isn't optional when dealing with payments. Here are some key practices to look out for when integrating QR payments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Verify Every Webhook Properly&lt;/strong&gt;&lt;br&gt;
Flutterwave sends payment notifications with a cryptographic signature. Before processing any webhook, check the &lt;code&gt;verif-hash&lt;/code&gt; header against your secret hash using HMAC-SHA256.. Without this verification, attackers could send fake payment confirmations to your system. Set up your secret hash in your Flutterwave dashboard under Settings &amp;gt; Webhooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Always Verify Transactions Server-Side&lt;/strong&gt;&lt;br&gt;
Never trust payment status from your mobile app. After receiving a webhook or when a customer claims payment was successful, make a server-side call to Flutterwave's verification &lt;a href="https://developer.flutterwave.com/v3.0.0/reference/verify-transaction-with-tx_ref" rel="noopener noreferrer"&gt;endpoint&lt;/a&gt; using the transaction ID. Check that the status is "successful," the amount matches what you expected, and the currency is correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Monitor Your Integration&lt;/strong&gt;&lt;br&gt;
Log all payment attempts, webhook receipts, and verification calls. Watch for patterns like multiple failed verification attempts or webhooks with invalid signatures. Set up alerts for unusual activity like payments from unexpected amounts or currencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Protect Your API Keys Like Passwords&lt;/strong&gt;&lt;br&gt;
Your Flutterwave secret key can process payments on your behalf. Store it securely on your server using environment variables or a secrets manager. Never embed secret keys in mobile app code where they can be extracted. All API calls requiring secret keys must originate from your secure backend server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The benefits of QR code payments are clear: faster checkouts, reduced cart abandonment, better user experience, and access to valuable transaction data. Additionally, implementation is straightforward and secure because of Flutterwave's powerful API.&lt;/p&gt;

&lt;p&gt;Remember the key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QR payments work through a simple scan-and-pay flow.&lt;/li&gt;
&lt;li&gt;Flutterwave provides easy-to-use APIs for QR code generation.&lt;/li&gt;
&lt;li&gt;Always verify payments server-side and implement proper security measures.&lt;/li&gt;
&lt;li&gt;Use webhooks for real-time payment notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready to transform how your customers pay? &lt;a href="https://developer.flutterwave.com/docs/getting-started" rel="noopener noreferrer"&gt;Explore Flutterwave's&lt;/a&gt; payment solutions and start building the future of payments today.&lt;/p&gt;

</description>
      <category>qrcode</category>
      <category>payment</category>
      <category>fintech</category>
      <category>flutterwave</category>
    </item>
    <item>
      <title>Supporting Offline Payments in Low-Connectivity Areas</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 10 Oct 2025 10:30:19 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/supporting-offline-payments-in-low-connectivity-areas-5065</link>
      <guid>https://forem.com/flutterwaveeng/supporting-offline-payments-in-low-connectivity-areas-5065</guid>
      <description>&lt;p&gt;You have built a payment integration that works for customers in Lagos, but what happens when your customer tries to pay from a remote fishing community like Brass in Bayelsa State, where network towers are sparse and 3G signals drop every few minutes? &lt;a href="https://www.itu.int/itu-d/reports/statistics/2024/11/10/ff24-internet-use/" rel="noopener noreferrer"&gt;Only 38%&lt;/a&gt; of people across Africa had internet access in 2024. Compare that to the global average of 68%, and you'll see how big the challenge is. &lt;/p&gt;

&lt;p&gt;In regions like these, connectivity is unpredictable, dropping out during important moments and returning when you least expect it. Despite these challenges, millions of people across Africa complete transactions, accept payments from customers, and keep their businesses running every single day. As fintech developers, our job is to make sure our payment systems work for everyone, not just those with perfect internet connections.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you’ll know exactly how offline payments work, how to set them up using Flutterwave’s existing tools, and how to keep every transaction secure, even when there’s no connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Offline Payments Work Without Internet Connectivity
&lt;/h2&gt;

&lt;p&gt;If you regularly make transactions online, there has surely been a moment when you had no internet connectivity and your transaction failed. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9v0e9z92l3qrtbaaes5e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9v0e9z92l3qrtbaaes5e.jpg" alt="Online payment flow" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a high level, offline payments work by allowing you to get value for products and services even when you don’t have an internet connection (Wi-Fi, cellular). You get a perceived experience of a successful transaction, while in the background, the transaction hasn’t been approved; instead, it’s encrypted and cached on the device, waiting for an internet connection to really process the payment.&lt;/p&gt;

&lt;p&gt;Think of offline payments like sending a letter through the postal system instead of an instant message. When internet connectivity drops, your payment system needs to become a reliable post office, securely storing transaction details until they can be delivered and processed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Tech Behind Accepting Offline Payments: Store and Forward (SAF)&lt;/strong&gt;&lt;br&gt;
The most common and reliable way to handle offline payments is a method called &lt;strong&gt;"Store and Forward" (SAF)&lt;/strong&gt;. It’s a hybrid approach that perfectly balances security with real-world business needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2q9as9xk141konq3l30.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2q9as9xk141konq3l30.jpg" alt="store and forward" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s how it works:&lt;/p&gt;

&lt;p&gt;When a point-of-sale (POS) terminal or mobile application loses its internet connection, it transitions into an offline mode. In this state, instead of declining card payments, it captures the transaction details, encrypts them immediately, and stores them securely on the device's local memory. From the customer's perspective, the transaction appears to be completed; the customer gets a receipt and their goods right away. However, no real-time communication with the bank or payment processor occurs at this stage.&lt;/p&gt;

&lt;p&gt;To fully grasp the differences, consider the following comparison between a standard online transaction and an offline SAF transaction. &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Online Transaction&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Offline (Store &amp;amp; Forward) Transaction&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authorization Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time (seconds)&lt;/td&gt;
&lt;td&gt;Delayed (minutes, hours, or days after the transaction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Risk of Decline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low risk; instant authorization result (success/decline)&lt;/td&gt;
&lt;td&gt;High risk; no real-time check, approval/decline unknown until sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Funds Settlement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Initiated immediately upon authorization&lt;/td&gt;
&lt;td&gt;Initiated only after successful delayed authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customer Experience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instant confirmation of payment success or failure&lt;/td&gt;
&lt;td&gt;Instant "apparent" success; payment could fail later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merchant Liability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low; protected by real-time authorization&lt;/td&gt;
&lt;td&gt;High; assumes 100% of the risk for declined transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key Components Of A Great Offline System&lt;/strong&gt;&lt;br&gt;
These are the key components to have in mind when building a great offline system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Encryption:&lt;/strong&gt; All payment data must be encrypted at rest using &lt;a href="https://developer.flutterwave.com/docs/encryption" rel="noopener noreferrer"&gt;AES-256&lt;/a&gt; or equivalent standards. This protects sensitive information even if devices are compromised while offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-&lt;/strong&gt;&lt;strong&gt;S&lt;/strong&gt;&lt;strong&gt;afe Mechanisms:&lt;/strong&gt; Your system needs fallback options when primary payment methods fail. This might include alternative payment channels like USSD or SMS-based confirmations that work over basic cellular networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Resolution Logic:&lt;/strong&gt; What happens if the same transaction tries to sync twice? Your system needs to be intelligent enough to identify duplicates, handle errors, and resolve conflicts without interruption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Data Management:&lt;/strong&gt; You can't keep transaction data on a device forever. A great offline system automatically and securely deletes old or processed transaction details to keep data safe.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How To Support Offline Payments
&lt;/h2&gt;

&lt;p&gt;Now let's get into the actual implementation. Since this is a custom setup, you’ll be handling the implementation and compliance, but we’ll walk you through everything you need to know. First, let’s cover what we’re building and the basic requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What You're Building&lt;/strong&gt;&lt;br&gt;
Let's say you're building a mobile app for small merchants selling goods at local markets. These merchants often work in areas where network coverage is spotty, meaning they sometimes have good connectivity and sometimes they don't. Your app needs to accept card payments regardless of network conditions.&lt;/p&gt;

&lt;p&gt;The system operates in two distinct phases: an initial online phase to securely capture and tokenize a customer's payment card and subsequent offline phases that use this token for transactions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn22g9h2s5zzu0oyrx5ae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn22g9h2s5zzu0oyrx5ae.png" alt="offline system architecture" width="800" height="645"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Technical Requirements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Client-Side Architecture&lt;/strong&gt;&lt;br&gt;
Your mobile application will be the core of the offline system and must be engineered to handle several key responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network State Detection:&lt;/strong&gt; The app must reliably detect when the device is offline and online to switch between modes automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Local Database:&lt;/strong&gt; An encrypted database is required to store a queue of pending transactions. Technologies like &lt;a href="https://sqlite.org/" rel="noopener noreferrer"&gt;SQLite&lt;/a&gt; with an encryption layer (e.g., &lt;a href="https://www.zetetic.net/sqlcipher/" rel="noopener noreferrer"&gt;SQLCipher&lt;/a&gt;) are suitable for this purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Queue Management:&lt;/strong&gt; The app needs to manage a queue of transaction objects, each with a status (e.g., &lt;code&gt;pending_sync&lt;/code&gt;, &lt;code&gt;sync_in_progress&lt;/code&gt;, &lt;code&gt;sync_successful&lt;/code&gt;, &lt;code&gt;sync_failed&lt;/code&gt;), a unique transaction reference, timestamp, and all necessary payment details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Risk Management Configuration&lt;/strong&gt;&lt;br&gt;
Given that the merchant assumes all financial risk, building configurable limits directly into the application is non-negotiable. These serve as critical financial fail-safes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-Transaction Limit:&lt;/strong&gt; A maximum monetary value that can be processed in a single offline transaction. Any amount exceeding this limit should be rejected, forcing an online attempt.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cumulative Offline Limit:&lt;/strong&gt; A ceiling for the total value of all transactions waiting in the offline queue. Once this limit is reached, the app must prevent further offline payments until the existing queue is successfully synced.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Limit:&lt;/strong&gt; A mandatory synchronization window, typically between 24 and 72 hours. Any transaction not uploaded within this period will expire and cannot be processed. Your application logic must enforce this deadline rigorously.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flutt&lt;/strong&gt;&lt;strong&gt;e&lt;/strong&gt;&lt;strong&gt;rwave Setup&lt;/strong&gt;&lt;br&gt;
Before you start building, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A live &lt;a href="https://onboarding.flutterwave.com/signup" rel="noopener noreferrer"&gt;Flutterwave account&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Your Public Key, Secret Key, and Encryption Key obtained from the &lt;a href="https://app.flutterwave.com/login" rel="noopener noreferrer"&gt;Flutterwave dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The relevant Flutterwave mobile SDK integrated into your project (e.g., &lt;a href="https://github.com/Flutterwave/AndroidSDK" rel="noopener noreferrer"&gt;Android SDK&lt;/a&gt;, &lt;a href="https://github.com/Flutterwave/iOS-v3" rel="noopener noreferrer"&gt;iOS SDK&lt;/a&gt;)
## Step-by-Step Implementation Guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnrk7zd2lilireiotskr7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnrk7zd2lilireiotskr7.jpg" alt="step by step implementation" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Acquiring the Payment Token&lt;/strong&gt;&lt;br&gt;
The golden rule here is that the &lt;strong&gt;very first payment from a customer must be online&lt;/strong&gt;. This first transaction is key because it allows Flutterwave to create a secure token for the customer’s card. You’ll use this token for all their future offline payments. This token, a non-sensitive placeholder, is what will be used for all subsequent offline payments. This design means the solution is tailored for recurring payments from returning customers, not for first-time interactions with new customers who are already offline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Before you continue, make sure you thoroughly understand &lt;a href="https://developer.flutterwave.com/v3.0.0/docs/tokenization" rel="noopener noreferrer"&gt;card tokenization&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initiate a Standard Online Charge:&lt;/strong&gt; Using the Flutterwave &lt;a href="https://github.com/Flutterwave" rel="noopener noreferrer"&gt;mobile SDK&lt;/a&gt;, perform a standard card payment. This process involves collecting the customer's card details within the secure UI provided by the SDK, which handles the encryption and communication with Flutterwave's servers.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the Transaction:&lt;/strong&gt; After the charge, &lt;a href="https://developer.flutterwave.com/v3.0.0/reference/verify-transaction" rel="noopener noreferrer"&gt;verify&lt;/a&gt; the payment status. The token will be available in the verification response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract and Store the Token&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt; In the verification response, you'll find the token in the &lt;code&gt;data.card.token&lt;/code&gt; field.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Example successful response structure:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charge successful"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;277036749&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tx_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new-live-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"flw_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FLW253481676"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"successful"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"payment_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;210745229&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Yemi Desola"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user@gmail.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"first_6digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"last_4digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MASTERCARD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"expiry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"08/22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flw-t1nf-f9b3bf384cd30d6fca42b6df9d27bd2f-m03k"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;4.&lt;/em&gt; &lt;strong&gt;Secure the Token:&lt;/strong&gt; Store this token (&lt;code&gt;flw-t1nf-...&lt;/code&gt;) along with the customer's email address in your local database. Both are required for future tokenized charges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Secure Storage and Transaction Capture&lt;/strong&gt;&lt;br&gt;
With a token securely stored, your application is now equipped to handle payments without an internet connection.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Secure Token Storage:&lt;/strong&gt; The card token must be stored using the most secure, hardware-backed mechanisms available on the mobile platform.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Android:&lt;/strong&gt; Use the &lt;a href="https://developer.android.com/privacy-and-security/keystore" rel="noopener noreferrer"&gt;Android Keystore&lt;/a&gt; system to generate and store encryption keys. These keys can then be used to encrypt the Flutterwave token and the transaction queue data, which can be stored in DataStore with Tink encryption for simple data or a database encrypted with SQLCipher for more complex queue management.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On iOS:&lt;/strong&gt; Use the native &lt;a href="https://developer.apple.com/documentation/security/keychain-services" rel="noopener noreferrer"&gt;Keychain Services&lt;/a&gt; to store the token and other sensitive transaction data. The Keychain is a secure, encrypted container managed by the operating system, designed specifically for credentials and small pieces of sensitive data.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement the Offline Checkout Flow:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;When the app detects it is offline, the payment UI should switch to an "Offline Mode."&lt;/li&gt;
&lt;li&gt;The user enters the transaction amount.&lt;/li&gt;
&lt;li&gt;Your app validates the amount against the pre-configured per-transaction and cumulative limits.&lt;/li&gt;
&lt;li&gt;Upon confirmation, the app creates a new record in its local, encrypted transaction queue. This record should include:

&lt;ul&gt;
&lt;li&gt;A unique transaction reference (&lt;code&gt;tx_ref&lt;/code&gt;) generated on the client&lt;/li&gt;
&lt;li&gt;The transaction amount and currency&lt;/li&gt;
&lt;li&gt;The customer's identifier and the associated card token&lt;/li&gt;
&lt;li&gt;A timestamp&lt;/li&gt;
&lt;li&gt;An initial status of &lt;code&gt;pending_sync&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Provide the customer with a receipt indicating the payment was accepted offline and will be processed once connectivity is restored.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Reconnecting (Online)&lt;/strong&gt; &lt;strong&gt;—&lt;/strong&gt; &lt;strong&gt;Synchronization and Processing&lt;/strong&gt;&lt;br&gt;
This final stage is where the stored transactions are forwarded to Flutterwave for actual processing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect Network Reconnection:&lt;/strong&gt; Implement a background service or a listener that triggers when the device's internet connection is restored.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process the Transaction Queue:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Once online, the service should read the pending transactions from the local database.&lt;/li&gt;
&lt;li&gt;For each transaction in the queue, construct and send a POST request to Flutterwave's &lt;code&gt;[v3/tokenized-charges](https://developer.flutterwave.com/v3.0.0/reference/charge-with-token-1)&lt;/code&gt; endpoint.
&lt;/li&gt;
&lt;li&gt;The body of the request will be populated with the data you stored locally, including the card token.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Example cURL request for a&lt;/em&gt; &lt;a href="https://developer.flutterwave.com/v3.0/docs/tokenization" rel="noopener noreferrer"&gt;&lt;em&gt;tokenized charge&lt;/em&gt;&lt;/a&gt;&lt;em&gt;:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--url&lt;/span&gt; https://api.flutterwave.com/v3/tokenized-charges &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer YOUR_SECRET_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
            "token": "flw-t1nf-f9b3bf384cd30d6fca42b6df9d27bd2f-m03k",
            "email": "user@example.com",
            "currency": "NGN",
            "country": "NG",
            "amount": 2000,
            "tx_ref": "your-unique-offline-tx-ref-123",
            "first_name": "Yemi",
            "last_name": "Desola",
            "narration": "Offline purchase of Item X"
         }'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;3.&lt;/em&gt; &lt;strong&gt;Handle Responses and Update the Queue:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Success (HTTP 200):&lt;/strong&gt; If the API call is successful and the transaction is approved by the bank, update the transaction's status in your local queue to &lt;code&gt;sync_successful&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Failure (HTTP 4xx/5xx):&lt;/strong&gt; Implement proper error handling.

&lt;ul&gt;
&lt;li&gt;If the transaction is definitively declined by the issuer (e.g., "Insufficient Funds," "Invalid Card"), update the status to &lt;code&gt;sync_failed&lt;/code&gt;. This transaction must be flagged for manual follow-up with the customer.
&lt;/li&gt;
&lt;li&gt;If the failure is due to a temporary network issue or a server error on the gateway's end, leave the status as &lt;code&gt;pending_sync&lt;/code&gt; and implement a &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/retry-backoff.html" rel="noopener noreferrer"&gt;retry mechanism&lt;/a&gt; with &lt;a href="https://en.wikipedia.org/wiki/Exponential_backoff" rel="noopener noreferrer"&gt;exponential backoff&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Once a transaction is successfully processed, the corresponding record should be securely cleared from the device to minimize data retention.
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Safety Practices for Offline Payments
&lt;/h2&gt;

&lt;p&gt;Building a custom offline payment solution introduces unique security challenges. Here are the practices to keep transactions safe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always Encrypt Everything:&lt;/strong&gt; Data must be protected everywhere. Use AES-256 to encrypt data stored on the device, and make sure you’re using &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" rel="noopener noreferrer"&gt;TLS 1.2+&lt;/a&gt; when the app communicates with Flutterwave’s servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Hardware-Level Security:&lt;/strong&gt; Store your encryption keys in the most secure place possible: the device’s hardware. Use Android’s Keystore and iOS’s Keychain services to protect your keys from attackers.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Smart Transaction Limits:&lt;/strong&gt; Since you can't check for fraud in real-time, your app needs to be the first line of defense. Build in hard limits for how much can be spent in a single offline transaction and a total limit for all pending payments. Think of these as safety switches that force the app to go online and sync before taking on more risk.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a Clear Audit Trail&lt;/strong&gt;: Things can go wrong, and you’ll need a record of every transaction. Log every offline attempt, every sync (successful or not), and the final payment status. This trail is essential for finding errors, handling disputes, and spotting fraud.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect Against a Lost or Stolen Device:&lt;/strong&gt; What happens if the merchant's device falls into the wrong hands? Your app needs to be locked down. &lt;strong&gt;Require a PIN, password, or biometric scan&lt;/strong&gt; (like a fingerprint) to open the app or make a payment. This simple step protects the stored data even if the phone itself is compromised. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Develop a Clear Data Deletion Strategy:&lt;/strong&gt; Don't keep sensitive data on a device longer than necessary. Once a transaction is successfully synced and its status is confirmed, your app should &lt;strong&gt;automatically and securely wipe it&lt;/strong&gt; from local storage. The less data on the device, the lower the risk.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;When your payment tool works everywhere, you open up your business to everyone. You’re able to serve merchants in remote markets, vendors during network outages, and millions of customers who can’t rely on a stable connection. It’s a huge step toward building a truly inclusive economy in Africa.&lt;/p&gt;

&lt;p&gt;The method we've covered gives you the foundation for a powerful offline payment feature. Yes, it requires careful security, smart limits, and solid sync logic. But the payoff is huge: payments that just work, no matter what.&lt;/p&gt;

&lt;p&gt;Ready to build experiences that work for everyone, everywhere? &lt;strong&gt;Get started with&lt;/strong&gt; &lt;a href="https://flutterwave.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Flutterwave&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>payments</category>
      <category>offline</category>
      <category>flutterwave</category>
      <category>internet</category>
    </item>
    <item>
      <title>Why Your App Needs To Collect Wallet Payments</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Mon, 29 Sep 2025 12:30:13 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/why-your-app-needs-to-collect-wallet-payments-16op</link>
      <guid>https://forem.com/flutterwaveeng/why-your-app-needs-to-collect-wallet-payments-16op</guid>
      <description>&lt;p&gt;Imagine you just completed building a mobile app for your business or client. The UI is smooth, the features work perfectly, and users are downloading your app at an incredible rate. But then you notice that 70% of users who add items to their cart never complete the purchase.&lt;/p&gt;

&lt;p&gt;While you focused on perfecting the core features, your checkout might be missing the payment methods your users actually prefer. The solution to this problem is to make sure your checkout supports a variety of payment options, including digital wallet payments like Google Pay and Apple Pay.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk you through integrating Apple Pay and Google Pay with Flutterwave, show you the code that makes it work, and talk about how adding digital wallet payment could be the difference between users who browse and users who buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Digital Wallets?
&lt;/h2&gt;

&lt;p&gt;A digital wallet is an app or service that stores your payment information (credit cards, debit cards, bank accounts) and lets you make payments without pulling out your actual cards. You can think of digital wallets like a physical wallet, but smarter. They don’t just store card numbers; they create secure, encrypted tokens that represent your payment methods. &lt;/p&gt;

&lt;p&gt;When you tap "Pay with Apple Pay," you're not sending your actual card details. Instead, your device sends a tokenized card number (stored securely on your device) along with a unique, one-time cryptogram that's generated fresh for each transaction. Even if someone intercepts this data, they can't reuse it for another purchase. You can learn more about how Apple Pay does this in &lt;a href="https://support.apple.com/en-gb/guide/security/sec82e7bc3f8/web" rel="noopener noreferrer"&gt;this guide.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Popular digital wallets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple Pay&lt;/strong&gt;: Built into every iPhone, iPad, and Apple Watch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Pay&lt;/strong&gt; (formerly Google Wallet): Default on Android devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samsung Pay&lt;/strong&gt;: Available on Samsung devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PayPal&lt;/strong&gt;: Cross-platform digital payments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional players&lt;/strong&gt;: Alipay and WeChat Pay in China, Paytm in India.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this guide, we’ll focus on Apple Pay and Google Pay integration, but first, let’s touch on why digital wallets matter more than you think, and then we’ll jump into some code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Digital Wallets Matter More Than You Think
&lt;/h2&gt;

&lt;p&gt;The standard checkout process is a major source of cart abandonment, and it looks like this for most users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User taps "Buy Now."&lt;/li&gt;
&lt;li&gt;User navigates to the checkout form.&lt;/li&gt;
&lt;li&gt;User struggles to type 16-digit card numbers on a small keyboard.&lt;/li&gt;
&lt;li&gt;User gets frustrated with multiple form fields (CVV, expiration, billing address).&lt;/li&gt;
&lt;li&gt;User abandons cart due to the cumbersome process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How Digital Wallets Solve the Biggest Checkout Problems&lt;/strong&gt;&lt;br&gt;
The reasons users abandon carts have been &lt;a href="https://baymard.com/lists/cart-abandonment-rate" rel="noopener noreferrer"&gt;extensively studied&lt;/a&gt;. The biggest checkout hurdles are mandatory sign-ups and clunky payment forms — forcing users to create an account is an instant buzzkill, causing a quarter of them to leave.&lt;/p&gt;

&lt;p&gt;Let's look at some of the most common problems and reasons why users abandon checkouts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Friction Point&lt;/th&gt;
&lt;th&gt;Abandonment Rate&lt;/th&gt;
&lt;th&gt;How Apple/Google Pay Solves It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mandatory Account Creation&lt;/td&gt;
&lt;td&gt;24–26%&lt;/td&gt;
&lt;td&gt;Inherently a "guest checkout" experience; no new account needed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long / Complicated Checkout Process&lt;/td&gt;
&lt;td&gt;17–22%&lt;/td&gt;
&lt;td&gt;Reduces a multi-step form to a single biometric authentication.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Concerns / Lack of Trust&lt;/td&gt;
&lt;td&gt;18–25%&lt;/td&gt;
&lt;td&gt;Uses tokenization; actual card numbers are never shared with the merchant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not Enough Payment Methods&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;Adds the user's preferred, most convenient payment method.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Website Errors / Crashes&lt;/td&gt;
&lt;td&gt;13–17%&lt;/td&gt;
&lt;td&gt;Reduces reliance on complex, custom-built form validation logic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra Costs (Shipping, Taxes)&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;While digital wallets can't eliminate costs, they make the total cost clearer upfront and reduce checkout friction once users have decided to purchase.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is precisely the kind of checkout friction that Apple Pay and Google Pay eliminate, reducing checkout to literally two taps: Face ID and confirm.&lt;/p&gt;

&lt;p&gt;The impact of this simple two-tap process is huge. It’s why mobile wallets are on track to process over &lt;a href="https://www.knowledge-sourcing.com/report/global-digital-wallet-market" rel="noopener noreferrer"&gt;&lt;strong&gt;$8 trillion&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;in transactions by 2025&lt;/strong&gt; and why businesses that integrate Apple Pay see their conversion rates jump by up to &lt;a href="https://stripe.com/blog/testing-the-conversion-impact-of-50-plus-global-payment-methods" rel="noopener noreferrer"&gt;22.3&lt;/a&gt;&lt;a href="https://stripe.com/blog/testing-the-conversion-impact-of-50-plus-global-payment-methods" rel="noopener noreferrer"&gt;&lt;strong&gt;%&lt;/strong&gt;&lt;/a&gt; compared to old-school credit card forms.&lt;/p&gt;

&lt;p&gt;Let’s dive into some code to guide you through integrating Apple Pay and Google Pay into your payment checkout.&lt;/p&gt;
&lt;h2&gt;
  
  
  Integrating Apple Pay/Google Pay with Flutterwave
&lt;/h2&gt;

&lt;p&gt;Integrating Apple Pay/Google Pay through Flutterwave is straightforward and can be completed in just three steps. To get started, you need the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;br&gt;
Before writing any code, make sure the following setup is complete:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dashboard.flutterwave.com/signup" rel="noopener noreferrer"&gt;&lt;strong&gt;Flutterwave Account&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; You should have a Flutterwave account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook URL:&lt;/strong&gt; You must have a webhook URL configured to receive final payment status updates from Flutterwave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable Apple Pay/Google Pay in Dashboard:&lt;/strong&gt; Navigate to your Flutterwave Dashboard, go to &lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Business Preferences&lt;/code&gt; &amp;gt; &lt;code&gt;Payment methods&lt;/code&gt;, scroll down to the wallets section, and enable Apple Pay/Google Pay. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzd25vged280ycug8fv0m.png" alt="Enable Apple Pay and Google Pay" width="800" height="314"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: We are using the &lt;a href="https://developer.flutterwave.com/docs/apple-pay" rel="noopener noreferrer"&gt;v3 flow&lt;/a&gt; for our integration in this guide; you can refer to the &lt;br&gt;
&lt;a href="https://developer.flutterwave.com/docs/payment-orchestrator-flow" rel="noopener noreferrer"&gt;orchestrator flow&lt;/a&gt; guide for v4.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Our&lt;/strong&gt; &lt;strong&gt;Payment Flow&lt;/strong&gt;&lt;br&gt;
Before we jump into the step-by-step implementation, let's visualize the entire payment process so you understand how all the pieces fit together. This overview will help you see where your code fits into the broader transaction flow and why each step is necessary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct14n1orc8etg0zfeyq6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct14n1orc8etg0zfeyq6.png" alt="Flutterwave digital wallet payment flow" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The v3 flow streamlines the payment process into three main steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initiate Charge&lt;/strong&gt;: Your server sends transaction details to the charges endpoint, specifying the wallet type (Apple Pay or Google Pay).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect User&lt;/strong&gt;: Flutterwave responds with an &lt;code&gt;auth_url&lt;/code&gt; that contains the redirect URL. Your client redirects the user to this URL where they'll see the payment button for their respective wallet.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykoraorgpjdf0x4fk0yu.webp" alt="Apple Pay button" width="406" height="114"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After clicking the button, Flutterwave presents the native Apple Pay payment sheet. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fckgnfz7mzp8tyhw3hrrq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fckgnfz7mzp8tyhw3hrrq.jpg" alt="Apple Pay Payment sheet" width="380" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Complete Payment (Verify transaction&lt;/strong&gt; &lt;a href="https://developer.flutterwave.com/docs/events-webhooks/webhooks" rel="noopener noreferrer"&gt;&lt;strong&gt;via Webhook&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;):&lt;/strong&gt; After the payment has been processed and is successful, Flutterwave processes it and sends a webhook to your server with the final transaction status. This webhook confirms the transaction's success or failure.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step-By-Step Guide To Set Apple Pay And Google Pay With Flutterwave
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Initiating the&lt;/strong&gt; &lt;strong&gt;Charge&lt;/strong&gt;&lt;br&gt;
The process begins with a server-side API call to Flutterwave's &lt;code&gt;/v3/charges&lt;/code&gt; endpoint. You specify the payment type using the &lt;code&gt;type&lt;/code&gt; query parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple Pay Request:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://api.flutterwave.com/v3/charges?type=applepay'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer YOUR_SECRET_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
            "amount": 100,
            "currency": "USD",
            "email": "jane.doe@example.com",
            "fullname": "Jane Doe",
            "tx_ref": "AP_UNIQUE_TRANSACTION_REF_001"
         }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Google Pay Request:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://api.flutterwave.com/v3/charges?type=googlepay'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer YOUR_SECRET_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
            "amount": 100,
            "currency": "USD",
            "email": "jane.doe@example.com",
            "fullname": "Jane Doe",
            "tx_ref": "GP_UNIQUE_TRANSACTION_REF_001"
         }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;amount&lt;/code&gt;: The total amount to charge&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;currency&lt;/code&gt;: Three-letter ISO currency code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt;: Customer's email address (required)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fullname&lt;/code&gt;: Customer's full name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tx_ref&lt;/code&gt;: A unique transaction reference generated on your end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Handling the Response and Redirecting&lt;/strong&gt;&lt;br&gt;
If your request is successful, Flutterwave will respond with a &lt;code&gt;200 OK&lt;/code&gt; status and a JSON body containing the payment details and redirect URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample Response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charge initiated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;645498756&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tx_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AP_UNIQUE_TRANSACTION_REF_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"flw_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TKVH48681032738026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"charged_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"app_fee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"auth_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://applepay.aq2-flutterwave.com?reference=TKVH48681032738026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"payment_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"applepay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-06-11T12:18:11.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;379560157&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane.doe@example.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"redirect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"redirect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://applepay.aq2-flutterwave.com?reference=TKVH48681032738026"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server should extract the redirect URL from either &lt;code&gt;data.auth_url&lt;/code&gt; or &lt;code&gt;data.meta.authorization.redirect&lt;/code&gt; and send it back to the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;responseFromServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user will then see the Apple Pay or Google Pay button on the hosted page. After clicking the button, they'll see the native payment sheet for their respective wallet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Verifying the Payment Status&lt;/strong&gt;&lt;br&gt;
This is the most critical part — your application must not rely on the redirect back to your site to confirm payment success. The only source of truth is the webhook from Flutterwave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample Webhook Payload:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"charge.completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;643032751&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tx_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AP_UNIQUE_TRANSACTION_REF_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"flw_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OIFW66891029265658"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"charged_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"successful"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"payment_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"applepay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"processor_response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"APPROVED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-06-07T16:41:28.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;377827143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane.doe@example.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"event.type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"APPLEPAY_TRANSACTION"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your webhook handler should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify the webhook signature (if configured)&lt;/li&gt;
&lt;li&gt;Check the &lt;code&gt;data.status&lt;/code&gt; field for &lt;code&gt;"successful"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verify the transaction using Flutterwave's verification endpoint&lt;/li&gt;
&lt;li&gt;Only then provide value to the customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Node.js Webhook Handler:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Verify the transaction&lt;/span&gt;
        &lt;span class="nf"&gt;verifyTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flw_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="c1"&gt;// Provide value to customer&lt;/span&gt;
              &lt;span class="nf"&gt;fulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tx_ref&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;})&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verification failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;To help you on your integration journey, here are some official resources from Flutterwave docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.flutterwave.com/v3.0.0/docs/apple-paytm%EF%B8%8F" rel="noopener noreferrer"&gt;&lt;strong&gt;Apple Pay Integration Guide&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Dive deeper into the specifics of integrating Apple Pay, including both the orchestrator and general flows.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.flutterwave.com/v3.0.0/docs/google-paytm%EF%B8%8F" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Pay Integration Guide&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Detailed documentation for adding Google Pay to your checkout process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Stop letting a clunky checkout dictate your revenue. By integrating Apple Pay and Google Pay, you can eliminate the biggest points of friction and convert more mobile browsers into buyers. Log into your &lt;a href="https://dashboard.flutterwave.com/signup" rel="noopener noreferrer"&gt;Flutterwave dashboard&lt;/a&gt; to enable wallet payments today, or check out the official API documentation to &lt;a href="https://developer.flutterwave.com/docs/getting-started" rel="noopener noreferrer"&gt;get started&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wallet</category>
      <category>payments</category>
      <category>fintech</category>
      <category>flutterwave</category>
    </item>
    <item>
      <title>Fintech Security: Best Practices for Fintech Apps</title>
      <dc:creator>uma victor</dc:creator>
      <pubDate>Fri, 19 Sep 2025 12:16:07 +0000</pubDate>
      <link>https://forem.com/flutterwaveeng/fintech-security-best-practices-for-fintech-apps-4k2i</link>
      <guid>https://forem.com/flutterwaveeng/fintech-security-best-practices-for-fintech-apps-4k2i</guid>
      <description>&lt;p&gt;Africa is in the middle of a fintech revolution, and you’re part of it. If you’re reading this, it most likely means you’re part of the people building and securing fintech applications. Your app empowers users and unlocks economic potential, leading to more growth in the fintech space. But with this growth in the financial services industry comes a serious risk of fraudulent activity.&lt;/p&gt;

&lt;p&gt;Nigerian financial institutions lost N52.26 billion to &lt;a href="https://fintechmagazine.africa/2025/02/26/nigerian-financial-institutions-lose-n52-26-billion-to-fraud-in-2024/" rel="noopener noreferrer"&gt;fraud&lt;/a&gt; in 2024. Kenya recorded 51% of mobile payment transactions &lt;a href="https://www.connectingafrica.com/mobile-money/kenya-sa-worst-hit-by-mobile-payment-fraud" rel="noopener noreferrer"&gt;as suspicious&lt;/a&gt;. One South African bank disclosed that it had &lt;a href="https://sekura.id/sim-swap-fraud-in-south-africa-2024/" rel="noopener noreferrer"&gt;reimbursed&lt;/a&gt; over R50 million to customers affected by SIM swap fraud last year, according to cybersecurity firm Sekura.id. &lt;/p&gt;

&lt;p&gt;These statistics highlight the complex environment you're building in. You're dealing with first-time digital banking users, unreliable network infrastructure, and sophisticated fraud syndicates that meticulously probe your authentication systems for vulnerabilities. The cost of getting security wrong goes beyond financial losses. When your app is someone's only connection to the banking system, a breach of sensitive information can have devastating consequences.&lt;/p&gt;

&lt;p&gt;This guide shows you how to build secure solutions that works in the African fintech space. You'll learn the specific threats facing fintech apps on the continent and get actionable techniques that companies like Flutterwave use to protect millions of users. &lt;/p&gt;

&lt;h2&gt;
  
  
  Common Threats for African Fintech Apps
&lt;/h2&gt;

&lt;p&gt;To build a strong defense against cyber threats, you must understand the enemy. The African threat landscape is unique. It is shaped by the continent's mobile-first economy. Let’s look at five threats that form the core challenge for fintech app security.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. SIM Swap Attacks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5p0hnn590rjidfimeax6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5p0hnn590rjidfimeax6.jpg" alt="SIM swap attack flow" width="800" height="1294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SIM swap fraud is a major threat to African fintech users. Attackers gather personal information through social engineering or data breaches. They call mobile operators pretending to be the victim and convince them to transfer the phone number to their SIM card. Once they control the phone number, they intercept SMS two-factor authentication codes and gain unauthorized access to financial accounts.&lt;/p&gt;

&lt;p&gt;The economics make SIM swap fraud attractive to criminals, because the cost to execute these attacks is minimal compared to potential victim losses that can reach thousands of dollars.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Infrastructure Vulnerabilities
&lt;/h3&gt;

&lt;p&gt;In many parts of Africa, infrastructure challenges create unique security considerations. Unstable internet means your app has to work offline, forcing you to save sensitive info directly on the user's phone. Without strong encryption, if that phone gets lost or stolen, bad actors now have the keys to sensitive data. This turns a helpful offline feature into a massive security hole.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Social Engineering
&lt;/h3&gt;

&lt;p&gt;Most successful attacks involve social engineering. On social media, hundreds of fake profiles impersonate official customer service representatives. They respond to customer complaints faster than real support teams and trick users into sharing personal information.&lt;/p&gt;

&lt;p&gt;Many African fintech users are experiencing digital banking for the first time. They lack cybersecurity awareness, and attackers exploit this with sophisticated phishing campaigns and fake customer service calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. API Security Gaps
&lt;/h3&gt;

&lt;p&gt;Small teams prioritize speed over security. APIs launch without proper authentication, encryption, or rate limiting. With limited development resources, security becomes an afterthought. Many small businesses incorporating digital financial services lack the technical expertise to properly secure their implementations. This creates widespread vulnerabilities across the fintech ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Regulatory Compliance Challenges
&lt;/h3&gt;

&lt;p&gt;Regulatory frameworks evolve rapidly across African markets. Keeping up with changing compliance requirements while maintaining security stretches resources thin for startups. Many struggle to meet basic regulatory standards, let alone implement advanced security measures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices for African Fintech App Security
&lt;/h2&gt;

&lt;p&gt;To defend against Africa’s unique threats, you need a multi-layered defense strategy. Here’s how:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; &lt;strong&gt;Reduce Reliance on SMS Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ykn60nzrmw4asdnde1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ykn60nzrmw4asdnde1v.png" alt="Insecure SMS authentication vs Secure Push Notification" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given the prevalence of SIM swap fraud, SMS-only authentication creates vulnerabilities. You need to combine SMS authentication with other multi-factor authentication methods (like biometrics). The goal is to keep authentication within a channel you control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use App-Based Push Notifications&lt;/strong&gt;: Send verification prompts directly to your app. This keeps the entire flow within your secure, end-to-end encrypted environment, completely bypassing the vulnerable mobile carrier network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement Biometrics&lt;/strong&gt;: Use fingerprint or facial recognition as a primary authentication factor. Biometric data is tied to the physical device and user, making it exponentially harder to compromise than a simple code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bind Accounts to Devices&lt;/strong&gt;: Use device fingerprinting to create a "trusted device" list for each user. If a login attempt comes from a new device, even with the correct password, you can block it or trigger a more rigorous step-up authentication challenge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example code snippet on how you can trigger a push notification for transaction verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifyTransaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Retrieve the user's unique, registered device push token from your database&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deviceToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserDeviceToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transaction Approval Required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Confirm a transaction of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="c1"&gt;// Add a timestamp to prevent replay attacks&lt;/span&gt;
          &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="c1"&gt;// Flag that this notification requires a biometric confirmation on the client-side&lt;/span&gt;
          &lt;span class="na"&gt;requiresBiometric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendPushNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deviceToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Encrypting Data Protection In Transit, at Rest, in Use&lt;/strong&gt; &lt;br&gt;
Assume every network is hostile and every database is a potential target. Data breaches are expensive, costing millions and eroding user trust permanently. &lt;strong&gt;End-to-end&lt;/strong&gt; &lt;a href="https://developer.flutterwave.com/docs/encryption" rel="noopener noreferrer"&gt;&lt;strong&gt;encryption&lt;/strong&gt;&lt;/a&gt; isn't optional.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data in Transit&lt;/strong&gt;: &lt;a href="https://www.cloudflare.com/en-gb/learning/ssl/why-use-tls-1.3/" rel="noopener noreferrer"&gt;Enforce&lt;/a&gt; &lt;a href="https://www.cloudflare.com/en-gb/learning/ssl/why-use-tls-1.3/" rel="noopener noreferrer"&gt;&lt;strong&gt;TLS 1.3&lt;/strong&gt;&lt;/a&gt; for all API communication. This ensures that data moving between your app and your servers is unreadable to attackers, protecting sensitive financial data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data at Rest&lt;/strong&gt;: Use industry-standard &lt;a href="https://www.progress.com/blogs/use-aes-256-encryption-secure-data" rel="noopener noreferrer"&gt;&lt;strong&gt;AES-256 encryption&lt;/strong&gt;&lt;/a&gt; for all stored data, from entire databases to file backups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field-Level Encryption&lt;/strong&gt;: Don't just encrypt the whole database. Encrypt individual sensitive fields (like national ID numbers or bank details) within your database records. This limits the "blast radius" if a part of your system is compromised.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following code provides a secure, modern way to handle field-level encryption in Node.js.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataEncryption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encryptionKeyHex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// A modern, authenticated encryption algorithm&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encryptionKeyHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Key must be 32 bytes for AES-256&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ivLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// GCM standard IV length&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nf"&gt;encryptField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ivLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Combine IV, authTag, and encrypted data. Storing them together is crucial for decryption.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nf"&gt;decryptField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cipherText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipherText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encryptedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDecipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAuthTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encryptedData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;decrypted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Tokenize Everything Sensitive&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoe4ja4vm39o1fm6b0qb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoe4ja4vm39o1fm6b0qb.png" alt="Tokenization flow" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.flutterwave.com/docs/card#tokenization" rel="noopener noreferrer"&gt;Tokenization&lt;/a&gt; is the process of replacing sensitive data with a non-sensitive equivalent, referred to as a "token." Think of it like a store gift card: it has no value outside that specific store, but inside it represents real money. If your database is breached, attackers get the useless tokens, not the actual customer financial data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payment Information&lt;/strong&gt;: Never store raw card numbers. Tokenize them immediately upon capture and work only with the tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal Data&lt;/strong&gt;: Tokenize national ID numbers, Bank Verification Numbers (BVNs), and other personally identifiable information (PII).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentTokenizer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// In a real system, tokenVault and auditLog would be highly secure,&lt;/span&gt;
      &lt;span class="c1"&gt;// isolated services (often PCI-DSS compliant).&lt;/span&gt;
      &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenVault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encryptionService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenVault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenVault&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auditLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encryption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;encryptionService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;tokenizeCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`tok_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Encrypt the sensitive data *before* storing it in the secure vault.&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encryptedCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;cardNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encryption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encryptField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;expiryDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encryption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encryptField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiry&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encryptedCard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;lastFourDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;cardType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectCardType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Secure Your APIs with a Zero-Trust Mindset&lt;/strong&gt; &lt;br&gt;
Your API is the front door to your entire system. Assume every request is malicious until proven otherwise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strong Authentication&lt;/strong&gt;: Use modern standards like &lt;strong&gt;OAuth 2.0&lt;/strong&gt; &lt;a href="https://oauth.net/2/pkce/" rel="noopener noreferrer"&gt;&lt;strong&gt;with PKCE&lt;/strong&gt;&lt;/a&gt; for mobile apps. Protect individual endpoints with short-lived &lt;strong&gt;JWT (JSON Web Tokens)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input Validation&lt;/strong&gt;: Validate and sanitize &lt;strong&gt;all&lt;/strong&gt; data coming from the client. Never trust the input. This is your primary defense against injection attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Rate Limiting&lt;/strong&gt;: Implement rate limits to prevent brute-force attacks. Your system should be smart enough to distinguish a user with a flaky connection from a malicious bot, perhaps by using a "leaky bucket" algorithm that allows for bursts of requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Risk Assessment&lt;/strong&gt;: Implement a risk scoring system that analyzes user behavior patterns, device fingerprints, IP addresses, and request anomalies. High-risk activities should trigger additional verification steps or temporary blocks.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;APISecurityMiddleware&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authentication token required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;

          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

          &lt;span class="c1"&gt;// A risk engine is a complex service that analyzes signals&lt;/span&gt;
          &lt;span class="c1"&gt;// like IP address, device fingerprint, user agent, and request patterns.&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;riskScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculateRiskScore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;riskScore&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// High risk: trigger step-up authentication (e.g., biometrics)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Additional verification required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;

          &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid or expired token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Monitor and Educate in Real-Time&lt;/strong&gt; &lt;br&gt;
The African context of lower digital literacy and sophisticated social engineering means you can't just build secure features; you must make your users part of the defense.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Fraud Detection&lt;/strong&gt;: Use machine learning models trained on local fraud patterns to monitor transactions. Flag or block activity that deviates from a user's normal behavior (e.g., unusual time, location, or amount).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual User Education&lt;/strong&gt;: Don't hide security tips in a forgotten FAQ page. Display them directly in the app. When a user is about to make their first transfer, show a tip: "Flutterwave will NEVER ask for your password or PIN in a phone call."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Language Support&lt;/strong&gt;: Provide security education in local languages. Security is for everyone, not just English speakers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Design for Intermittent Connectivity&lt;/strong&gt; &lt;br&gt;
As internet penetration continues to expand across Africa, many regions still face connectivity challenges that require offline-capable solutions. Your app's security model must not break when the user goes offline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt Local Data&lt;/strong&gt;: Any data cached or stored locally on the device, even temporarily, must be encrypted using the device's secure storage capabilities (like &lt;a href="https://developer.android.com/privacy-and-security/keystore" rel="noopener noreferrer"&gt;Android's Keystore&lt;/a&gt; or &lt;a href="https://developer.apple.com/documentation/security/keychain-services" rel="noopener noreferrer"&gt;iOS's Keychain&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Transaction Queuing&lt;/strong&gt;: If a user initiates a payment offline, the transaction details must be encrypted and stored securely until a connection is restored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Integrity Checks&lt;/strong&gt;: When the app reconnects, it must verify that locally stored data hasn't been tampered with while offline, perhaps by checking against a server-side hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To stay ahead of cyber attackers, fintech companies must also perform regular security audits and penetration testing to identify and fix potential vulnerabilities before they can be exploited. Adopting secure coding guidelines and a good security posture from day one is vital for long-term success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Cybercrime is a challenge that the fintech industry has to grapple with because it will not go away. The key to success lies in understanding that fintech security is not a destination but a continuous journey of improvement and adaptation.&lt;/p&gt;

&lt;p&gt;Remember: every security measure you implement today protects not just your users' money, but their trust in the digital financial system that's transforming Africa's economy.&lt;/p&gt;

&lt;p&gt;Want to learn more about building secure fintech solutions? Explore Flutterwave’s &lt;a href="https://developer.flutterwave.com/docs/best-practices" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt; for implementation guides and security best practices.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>security</category>
      <category>payments</category>
      <category>flutterwave</category>
    </item>
  </channel>
</rss>
