<?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>DEV Community: Victory Lucky</title>
    <description>The latest articles on DEV Community by Victory Lucky (@codewithveek).</description>
    <link>https://dev.to/codewithveek</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2762099%2F94f62399-fdfd-4a20-913d-edbf9373f524.png</url>
      <title>DEV Community: Victory Lucky</title>
      <link>https://dev.to/codewithveek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codewithveek"/>
    <language>en</language>
    <item>
      <title>How to Accept International Payments as an African Developer or Business</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Thu, 30 Jul 2026 18:44:40 +0000</pubDate>
      <link>https://dev.to/afriex/how-to-accept-international-payments-as-an-african-developer-or-business-33eh</link>
      <guid>https://dev.to/afriex/how-to-accept-international-payments-as-an-african-developer-or-business-33eh</guid>
      <description>&lt;p&gt;If you're an African developer, SaaS founder, freelancer, or online business, one of the biggest challenges isn't finding customers. It's getting paid by them.&lt;/p&gt;

&lt;p&gt;Most articles about African payment APIs focus on moving money out of Africa. But what if your customers are the ones sending money to you?&lt;/p&gt;

&lt;p&gt;Whether you're billing international clients, collecting subscription payments, or accepting payments from marketplace users, you need a collection method that's easy for customers and simple to reconcile on your end.&lt;/p&gt;

&lt;p&gt;The Afriex Business API offers three different ways to collect payments, each designed for a different use case. Depending on who your customers are and how they prefer to pay, you can collect funds through dedicated virtual accounts, shared pool accounts, or stablecoin wallets.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how each collection method works, when to use it, and how to integrate it into your application. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Collection Methods
&lt;/h2&gt;

&lt;p&gt;Although all three collection methods ultimately deposit funds into your Afriex Business wallet, they differ in how customers send money and how you identify who made each payment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;How the payer sends&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated virtual account&lt;/td&gt;
&lt;td&gt;Known customers, repeat payments&lt;/td&gt;
&lt;td&gt;Bank transfer to a unique account number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pool account&lt;/td&gt;
&lt;td&gt;Quick collection, one-off payments&lt;/td&gt;
&lt;td&gt;Bank transfer with a reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crypto wallet&lt;/td&gt;
&lt;td&gt;Customers holding USDT or USDC&lt;/td&gt;
&lt;td&gt;Crypto transfer to a wallet address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choosing the right method depends on your product and your payment flow. If you already know your customers and expect them to pay repeatedly, dedicated virtual accounts provide the smoothest experience. If you want to launch quickly without creating individual accounts for every customer, pool accounts are a great fit. And if your users prefer paying with stablecoins, crypto wallets make that process straightforward. &lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Dedicated Virtual Accounts
&lt;/h2&gt;

&lt;p&gt;Dedicated virtual accounts are the easiest way to reconcile bank transfers from repeat customers.&lt;/p&gt;

&lt;p&gt;Instead of asking every customer to send money to the same account and include a payment reference, each customer receives their own unique bank account number. Any payment that arrives in that account automatically belongs to that customer, so there's no manual matching or reconciliation required.&lt;/p&gt;

&lt;p&gt;This approach works especially well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS products with recurring subscriptions&lt;/li&gt;
&lt;li&gt;Marketplaces that maintain seller balances&lt;/li&gt;
&lt;li&gt;Businesses collecting repeat payments from customers&lt;/li&gt;
&lt;li&gt;Platforms serving African businesses that pay through bank transfers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Afriex supports two types of dedicated virtual accounts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static virtual accounts&lt;/strong&gt; remain permanently assigned to a customer, making them ideal for ongoing billing relationships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic virtual accounts&lt;/strong&gt; are temporary. They're tied to a specific payment amount and expire after a short period, making them a good choice for invoices and one-time payments. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating a Static or Dynamic Virtual Account
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Static virtual account — permanent, for repeat payments&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staticAccount&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualAccount&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COLLECTIONS&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;// Dynamic virtual account — expires after a short window, for a single invoice&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamicAccount&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualAccount&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&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;150000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// exact amount expected&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Both return a real account number and bank name to share with the customer&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="nx"&gt;staticAccount&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;accountNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "3820404958"&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="nx"&gt;staticAccount&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;institution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;institutionName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "FIDELITY BANK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you've created the account, share the returned &lt;strong&gt;account number&lt;/strong&gt; and &lt;strong&gt;bank name&lt;/strong&gt; with your customer. Behind the scenes, Afriex automatically associates every payment with the customer that owns that virtual account, eliminating the need for payment references or manual reconciliation. &lt;/p&gt;

&lt;h2&gt;
  
  
  Reusing Existing Accounts
&lt;/h2&gt;

&lt;p&gt;Creating static virtual accounts is idempotent. If you request another static account for the same customer and currency, Afriex returns the existing account instead of generating a new one.&lt;/p&gt;

&lt;p&gt;A good practice is to check whether an account already exists before creating one.&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;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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listVirtualAccounts&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&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;if &lt;/span&gt;&lt;span class="p"&gt;(&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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 one&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualAccount&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COLLECTIONS&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;This approach keeps your integration predictable while ensuring each customer maintains a single permanent account for recurring payments. &lt;/p&gt;

&lt;h3&gt;
  
  
  NGN Requirements
&lt;/h3&gt;

&lt;p&gt;If you're creating &lt;strong&gt;static NGN virtual accounts&lt;/strong&gt;, the customer must have a &lt;strong&gt;Bank Verification Number (BVN)&lt;/strong&gt; on file. This is a regulatory requirement set by the Central Bank of Nigeria.&lt;/p&gt;

&lt;p&gt;You can provide the BVN when creating the customer or update it later through the customer KYC endpoint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dynamic virtual accounts don't have this requirement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the time of writing, dedicated virtual accounts has support for &lt;strong&gt;NGN&lt;/strong&gt;, &lt;strong&gt;USD&lt;/strong&gt;, &lt;strong&gt;GBP&lt;/strong&gt;, and &lt;strong&gt;EUR&lt;/strong&gt; accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Pool Accounts
&lt;/h2&gt;

&lt;p&gt;Pool accounts are the fastest way to start accepting bank transfers when you don't need a dedicated account number for every customer.&lt;/p&gt;

&lt;p&gt;Instead of creating individual virtual accounts, Afriex provides a shared account for a specific country. Every customer pays into the same account, while a unique payment reference identifies who the payment belongs to.&lt;/p&gt;

&lt;p&gt;This approach significantly reduces the setup required, making it a great choice for products that need to start collecting payments quickly.&lt;/p&gt;

&lt;p&gt;The trade-off is that your application becomes responsible for keeping track of the payment reference associated with each customer. Though as long as those references are stored correctly, reconciling incoming deposits remains straightforward. &lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving a Pool Account
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&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;poolAccount&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listPoolAccounts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&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;// Share both pieces with the customer&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="nx"&gt;poolAccount&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;accountNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// the shared account number&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="nx"&gt;poolAccount&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;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// unique to this customer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike dedicated virtual accounts, there isn't a separate account number for each customer. Everyone transfers money to the same bank account, but each customer receives their own unique &lt;strong&gt;reference&lt;/strong&gt;, which must be included when making the payment.&lt;/p&gt;

&lt;p&gt;When the transfer arrives, Afriex uses that reference to identify which customer made the payment and credits it accordingly. &lt;/p&gt;

&lt;h3&gt;
  
  
  Store the Reference Before Sharing It
&lt;/h3&gt;

&lt;p&gt;The payment reference is the key to matching an incoming transfer to the correct customer. Because of that, you should save it immediately after retrieving the pool account details.&lt;/p&gt;

&lt;p&gt;A good pattern is to persist the payment details first, then send the payment instructions to the customer.&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="c1"&gt;// Store reference before sharing account details with customer&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pendingPayments&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;afriexReference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;poolAccount&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;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50000&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;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="c1"&gt;// Then share account details with your customer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentInstructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;poolAccount&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;institution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;institutionName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;poolAccount&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;accountNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;poolAccount&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;accountName&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;poolAccount&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;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// customer MUST include this&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;50000&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;Persisting the reference helps ensure every transfer can be matched correctly, even if the customer completes the payment hours or days later. &lt;/p&gt;

&lt;h3&gt;
  
  
  Country-Based Accounts
&lt;/h3&gt;

&lt;p&gt;Pool accounts are provisioned per country. You simply provide the appropriate country code, and Afriex returns the shared account for that market.&lt;/p&gt;

&lt;p&gt;There's no need to specify the currency separately because it's derived automatically from the country. For example, passing &lt;code&gt;NG&lt;/code&gt; returns an account that accepts &lt;strong&gt;NGN&lt;/strong&gt; payments. &lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Crypto Wallets (USDT / USDC)
&lt;/h2&gt;

&lt;p&gt;Not every customer prefers paying through a bank transfer. Some already hold stablecoins and would rather pay directly from their crypto wallet.&lt;/p&gt;

&lt;p&gt;For these customers, Afriex lets you generate dedicated wallet addresses for supported stablecoins such as &lt;strong&gt;USDT&lt;/strong&gt; and &lt;strong&gt;USDC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When funds arrive, Afriex automatically converts the deposit to its USD equivalent and records it as a &lt;strong&gt;DEPOSIT&lt;/strong&gt; transaction in your business wallet. That means you don't need to perform a separate conversion step before reconciling payments. &lt;/p&gt;

&lt;h3&gt;
  
  
  Creating or Retrieving a Customer Wallet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Get or create a USDT wallet for a specific customer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCryptoWallet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-customer-id&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="nx"&gt;wallet&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;addresses&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Returns addresses on multiple networks, e.g:&lt;/span&gt;
&lt;span class="c1"&gt;// [&lt;/span&gt;
&lt;span class="c1"&gt;//   { address: "0x1234...5678", network: "ETHEREUM_MAINNET" },&lt;/span&gt;
&lt;span class="c1"&gt;//   { address: "TYASr5...HLS",  network: "TRON_MAINNET" }&lt;/span&gt;
&lt;span class="c1"&gt;// ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contains wallet addresses for multiple blockchain networks. Share the address that matches the network your customer intends to use.&lt;/p&gt;

&lt;p&gt;For many customers, &lt;strong&gt;Tron&lt;/strong&gt; is the preferred option because transaction fees are generally lower than on Ethereum.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The wallet creation endpoint is idempotent, so requesting a wallet for the same customer and asset returns the same address instead of creating a new one each time. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Detecting Crypto Deposits
&lt;/h3&gt;

&lt;p&gt;Crypto deposits work a little differently from bank transfers.&lt;/p&gt;

&lt;p&gt;Instead of sending a webhook when funds arrive, you'll need to periodically check your transaction history for new &lt;strong&gt;DEPOSIT&lt;/strong&gt; records.&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;const&lt;/span&gt; &lt;span class="nx"&gt;recentDeposits&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DEPOSIT&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;Your application can poll this endpoint at a suitable interval and process any newly discovered deposits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep in mind that crypto wallets are available only in production and require the customer to complete verification before a wallet can be created. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Knowing When a Deposit Has Landed
&lt;/h2&gt;

&lt;p&gt;Collecting payments is only one part of the integration. Your application also needs a reliable way to know when funds have actually arrived so you can update balances, fulfill orders, or unlock access for customers.&lt;/p&gt;

&lt;p&gt;For payments made through &lt;strong&gt;dedicated virtual accounts&lt;/strong&gt; and &lt;strong&gt;pool accounts&lt;/strong&gt;, Afriex sends a webhook whenever a deposit is received. Instead of repeatedly checking for new transactions, your application can simply listen for these events and react as they happen. &lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Deposit Webhooks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&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;Request&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;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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-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;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="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;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="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="s2"&gt;Missing signature&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;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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Read raw body before any parsing — signature is verified against these exact bytes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;event&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="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyAndParse&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="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="k"&gt;return&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="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="s2"&gt;Invalid signature&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;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="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="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="s2"&gt;TRANSACTION.CREATED&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;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="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DEPOSIT&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="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="nx"&gt;destinationAmount&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="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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Attribute the deposit to the right customer and update your records&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;recordDeposit&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="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;destinationAmount&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="nx"&gt;destinationCurrency&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;return&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="na"&gt;received&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few implementation details are worth paying attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always verify the webhook signature before processing the request.&lt;/li&gt;
&lt;li&gt;Read the raw request body before parsing it. Signature verification depends on the original payload.&lt;/li&gt;
&lt;li&gt;Respond quickly after processing the event. Long-running work should happen asynchronously whenever possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These practices help keep your webhook endpoint secure and reliable in production. &lt;/p&gt;

&lt;h3&gt;
  
  
  Registering Your Webhook
&lt;/h3&gt;

&lt;p&gt;Once your endpoint is ready, register it in the Afriex dashboard under &lt;strong&gt;Developers &amp;gt; Webhooks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You'll also need to configure your application with the &lt;code&gt;AFRIEX_WEBHOOK_PUBLIC_KEY&lt;/code&gt;, which is used to verify incoming webhook signatures.&lt;/p&gt;

&lt;p&gt;Your webhook should return an HTTP &lt;strong&gt;200&lt;/strong&gt; response within five seconds. If it doesn't, Afriex automatically retries delivery using exponential backoff, giving your application multiple chances to process the event successfully. &lt;/p&gt;

&lt;p&gt;For a complete walkthrough of every available webhook event and recommended handling patterns, see the &lt;strong&gt;&lt;a href="https://dev.to/afriex/afriex-webhook-integration-guide-signature-verification-event-handling-and-production-best-3c9o"&gt;Afriex webhook integration guide&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Collection Method
&lt;/h2&gt;

&lt;p&gt;Each payment method solves a different problem, so the right choice depends on how your customers pay and how much reconciliation you want your application to handle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Dedicated Virtual Accounts if...
&lt;/h3&gt;

&lt;p&gt;Choose dedicated virtual accounts when your customers are known ahead of time and are likely to make repeat payments.&lt;/p&gt;

&lt;p&gt;Since every customer receives a unique bank account number, incoming transfers are automatically attributed to the correct customer. This eliminates manual reconciliation and provides the smoothest experience for subscription platforms, marketplaces, and businesses with recurring billing. &lt;/p&gt;

&lt;h3&gt;
  
  
  Use Pool Accounts if...
&lt;/h3&gt;

&lt;p&gt;Pool accounts are the better option when you want to start accepting payments quickly without creating virtual accounts for every customer.&lt;/p&gt;

&lt;p&gt;Although you'll need to store and match payment references yourself, the integration is lighter and requires less operational overhead, making it an excellent choice for products that prioritize speed of implementation. &lt;/p&gt;

&lt;h3&gt;
  
  
  Use Crypto Wallets if...
&lt;/h3&gt;

&lt;p&gt;Crypto wallets are ideal when your customers already hold &lt;strong&gt;USDT&lt;/strong&gt; or &lt;strong&gt;USDC&lt;/strong&gt; and prefer paying with stablecoins instead of making bank transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn More
&lt;/h2&gt;

&lt;p&gt;If you like to explore each payment method in more detail, these guides provide a deeper look at their individual integration flows:&lt;/p&gt;

&lt;p&gt;*&lt;br&gt;
  &lt;a href="https://dev.to/afriex/how-to-accept-usdt-and-usdc-payments-and-settle-automatically-in-usd-with-afriex-2m79"&gt;How to accept USDT and USDC&lt;/a&gt;&lt;br&gt;
  &lt;a href="https://dev.to/afriex/afriex-virtual-accounts-a-developers-guide-to-dedicated-and-pool-accounts-260b"&gt;Virtual accounts and pool accounts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also explore the Afriex Business platform and API documentation here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://business.afriex.com" rel="noopener noreferrer"&gt;https://business.afriex.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.afriex.com" rel="noopener noreferrer"&gt;https://docs.afriex.com&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;There's no single payment collection method that's right for every product.&lt;/p&gt;

&lt;p&gt;If your business relies on repeat customers, dedicated virtual accounts provide the simplest reconciliation experience. If speed matters more than individual account management, pool accounts let you start collecting payments with minimal setup. And if your customers already use stablecoins, crypto wallets offer a familiar way to pay while Afriex handles the conversion behind the scenes.&lt;/p&gt;

&lt;p&gt;By offering multiple collection methods through a single API, you can support different customer preferences without maintaining separate payment integrations.&lt;/p&gt;

&lt;p&gt;Whether you're building a SaaS platform, marketplace, fintech product, or any business that serves customers across borders, the Afriex Business API gives you the flexibility to accept payments in the way that best fits your users.&lt;/p&gt;

</description>
      <category>crossborder</category>
      <category>afriex</category>
      <category>payments</category>
      <category>api</category>
    </item>
    <item>
      <title>AI Agent observability with DriftWatch and SigNoz</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Sun, 26 Jul 2026 14:36:01 +0000</pubDate>
      <link>https://dev.to/codewithveek/ai-agent-observability-with-driftwatch-and-signoz-f8p</link>
      <guid>https://dev.to/codewithveek/ai-agent-observability-with-driftwatch-and-signoz-f8p</guid>
      <description>&lt;p&gt;In the agentic AI era, one of the major challenges is token and agent optimization, but then how can you truly optimize this without deep insights into the activities of your AI agent?&lt;br&gt;
Those were my thoughts while exploring agentic AI and MCP servers (I recently built &lt;a href="https://github.com/codewithveek/afriex_sdk/tree/main/packages/mcp-server" rel="noopener noreferrer"&gt;one for a Fintech&lt;/a&gt;), &lt;br&gt;
I knew the problem exists but I never truly understood the best way to approach it nor the tools required to solve that problem, until recently when I came across this tweet on X &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc68mhgnzgfqj7v78v3aq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc68mhgnzgfqj7v78v3aq.png" alt="a screenshot showing a post on X about agent telemetry" width="720" height="1640"&gt;&lt;/a&gt;Coincidentally, I got an email from WeMakeDevs about the Signoz hackathon on the same day I saw the above tweet, then it clicked that this was the right time to solve this problem, so I built DriftWatch.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most agentic AI applications are built blindly without deep insight into, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What tools were called most&lt;/li&gt;
&lt;li&gt;How long did it take (latency, throughput)&lt;/li&gt;
&lt;li&gt;How much tokens were consumed for input and output&lt;/li&gt;
&lt;li&gt;How much did it cost in USD?&lt;/li&gt;
&lt;li&gt;Which prompts consumed the most tokens&lt;/li&gt;
&lt;li&gt;Which model was used, and&lt;/li&gt;
&lt;li&gt;When did the agent start drifting from the intended action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This and more are the problems DriftWatch is solving.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what is DriftWatch and how does it solve the problems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DriftWatch&lt;/strong&gt; is a self-observing AI agent SDK that integrates with the &lt;a href="https://ai-sdk.dev/" rel="noopener noreferrer"&gt;Vercel AI SDK&lt;/a&gt;. &lt;br&gt;
It traces every tool call and model step an agent makes as OpenTelemetry into SigNoz, &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypiwv8t5todmnu709pea.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypiwv8t5todmnu709pea.PNG" alt="Signoz dashboard showing driftwatch tool call traces" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
then let's a &lt;strong&gt;drift-judge&lt;/strong&gt; watch that telemetry for behavioral drift by querying the SigNoz API, and detects shifts in tool-call mix, error rate, latency, or token spend.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;detectBehavioralDrift&lt;/span&gt; &lt;span class="p"&gt;}&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;@driftwatch/sdk&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;report&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;detectBehavioralDrift&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modelClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;driftDetectionConfig&lt;/span&gt;&lt;span class="p"&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;driftDetection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// SigNoz URL + API key&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="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   drift: true,&lt;/span&gt;
&lt;span class="c1"&gt;//   severity: 'medium',&lt;/span&gt;
&lt;span class="c1"&gt;//   reasons: ['error rate 1% → 8%', 'p95 latency 210ms → 680ms'],&lt;/span&gt;
&lt;span class="c1"&gt;//   recommended_action: 'Investigate the rising error rate before it spreads.'&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Along with this, DriftWatch also provides per-task guardrails, which lets you control the max token and cost of each task and what to do when that limit is met,&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;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;runAgentTask&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;guardrails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxTokensPerTask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// 0 disables this check&lt;/span&gt;
    &lt;span class="na"&gt;maxCostUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// 0 disables; derived from the prices below&lt;/span&gt;
    &lt;span class="na"&gt;pricePer1kInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pricePer1kOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onExceed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// 'stop' halts mid-loop; 'flag' finishes and marks it&lt;/span&gt;
  &lt;span class="p"&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guardrailTriggered&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guardrail hit:&lt;/span&gt;&lt;span class="dl"&gt;'&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;guardrailReason&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;
  
  
  How to use DriftWatch and connect to Signoz
&lt;/h2&gt;

&lt;p&gt;Using &lt;strong&gt;DriftWatch&lt;/strong&gt; and connecting it to &lt;strong&gt;SigNoz&lt;/strong&gt; is straightforward, and can be done in a few ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;p&gt;Install the SDK from npm, along with any ai provider of your choice&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @driftwatch/sdk ai zod
npm &lt;span class="nb"&gt;install&lt;/span&gt; @ai-sdk/openai   &lt;span class="c"&gt;# or any AI SDK provider you already use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Provide env variables and set up the telemetry
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Connecting to SigNoz Cloud&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;&lt;span class="c"&gt;# Ingestion: For pushing telemetry to SigNoz&lt;/span&gt;
&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://ingest.&amp;lt;region&amp;gt;.signoz.cloud:443
&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;signoz-ingestion-key&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-ingestion-key&amp;gt;

&lt;span class="c"&gt;# Query API: For pulling data for drift behaviour (create a key with at least the Viewer role)&lt;/span&gt;
&lt;span class="nv"&gt;SIGNOZ_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://&amp;lt;your-team&amp;gt;.&amp;lt;region&amp;gt;.signoz.cloud
&lt;span class="nv"&gt;SIGNOZ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-api-key&amp;gt;

&lt;span class="c"&gt;# Read by your provider package (@ai-sdk/openai), not by the SDK itself.&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Connecting to self-hosted SigNoz&lt;/strong&gt;&lt;br&gt;
Self-hosted SigNoz bundles the collector and query service, and needs no ingestion header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/SigNoz/signoz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;signoz/deploy/docker
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;          &lt;span class="c"&gt;# UI + query service on :8080, OTLP collector on :4318&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ingestion: For pushing telemetry to SigNoz&lt;/span&gt;
&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4318     &lt;span class="c"&gt;# collector (HTTP/proto)&lt;/span&gt;
&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;                            &lt;span class="c"&gt;# none for self-hosted&lt;/span&gt;


&lt;span class="c"&gt;# Query API: For pulling data for drift behaviour (create a key with at least the Viewer role)&lt;/span&gt;
&lt;span class="nv"&gt;SIGNOZ_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080                       &lt;span class="c"&gt;# query service / UI&lt;/span&gt;
&lt;span class="nv"&gt;SIGNOZ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;key from Settings → API Keys&amp;gt;

&lt;span class="c"&gt;# Read by your provider package (@ai-sdk/openai), not by the SDK itself.&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start telemetry, create a &lt;code&gt;telemetry.ts&lt;/code&gt; file (you can name the file anything)&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="c1"&gt;// telemetry.ts — preload; must run before anything else imports&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// load .env into process.env before anything reads it&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bootstrapTelemetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadDriftWatchConfigFromEnv&lt;/span&gt; &lt;span class="p"&gt;}&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;@driftwatch/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;bootstrapTelemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;loadDriftWatchConfigFromEnv&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bootstrapTelemetry&lt;/code&gt; starts OpenTelemetry and sends traces, metrics, and logs to SigNoz. It must run &lt;strong&gt;before&lt;/strong&gt; your application code, in a&lt;br&gt;
&lt;strong&gt;separate file&lt;/strong&gt;, loaded with &lt;code&gt;--import&lt;/code&gt;. This is not a style choice: OpenTelemetry instruments modules (&lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, …) by patching them the moment they load. If your application file has already imported those modules by the time &lt;code&gt;bootstrapTelemetry&lt;/code&gt; runs, the patch is too late, and nothing is captured.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3. Run an agent task
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;runAgentTask&lt;/code&gt; function runs a traced tool-use loop. Give it a model and tools. It returns the result and a usage summary, and because &lt;code&gt;telemetry.ts&lt;/code&gt; already started OpenTelemetry, it emits the &lt;code&gt;agent.run&lt;/code&gt; trace and &lt;code&gt;agent.tool.*&lt;/code&gt; metrics as it goes.&lt;/p&gt;

&lt;p&gt;The example below is runnable. The tool calls Open-meteo's public API.&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;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runAgentTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadDriftWatchConfigFromEnv&lt;/span&gt; &lt;span class="p"&gt;}&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;@driftwatch/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="p"&gt;}&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;@ai-sdk/openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="p"&gt;}&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;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadDriftWatchConfigFromEnv&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;modelClient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Tools the agent can call. Both hit Open-Meteo's free, keyless API — https://open-meteo.com.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;description&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 current weather for a city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;execute&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;city&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;geoRes&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://geocoding-api.open-meteo.com/v1/search?name=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;count=1`&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;geoData&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;geoRes&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;geoData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&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;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;location&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No location found for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;city&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weatherRes&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.open-meteo.com/v1/forecast?latitude=&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;latitude&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;longitude=&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;longitude&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;current_weather=true`&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;weatherData&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;weatherRes&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;city&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;country&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;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tempC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;weatherData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_weather&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;windSpeedKmh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;weatherData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_weather&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;windspeed&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="na"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search the Open-Meteo API documentation for a query term&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;execute&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;query&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;res&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://open-meteo.com/en/docs&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;html&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&amp;gt;/g&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;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.*+?^${}()|[&lt;/span&gt;&lt;span class="se"&gt;\]\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&amp;amp;&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;i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hits&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;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;runAgentTask&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What is the weather in Lagos, and how many times does the Open-Meteo docs page mention "temperature"?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;modelClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// any AI SDK provider; needs its API key&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxSteps&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseText&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skillsUsed&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;tokenUsage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;agent.ts&lt;/code&gt; with &lt;code&gt;telemetry.ts&lt;/code&gt; preloaded ahead of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx &lt;span class="nt"&gt;--import&lt;/span&gt; ./telemetry.ts ./agent.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it, You get the model's answer and the exact usage for that call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The current weather in Lagos, Nigeria is **27°C** with a wind speed of **15 km/h**.

The Open-Meteo documentation mentions the word "temperature" **59 times**.
[ 'get_weather', 'search_docs' ] { inputTokens: 877, outputTokens: 254, totalTokens: 1131 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it a few minutes, search &lt;code&gt;service_name=driftwatch&lt;/code&gt; from the SigNoz dashboard, and watch the traces, logs and metrics show up,&lt;br&gt;
if you need an already configured dashboard, import &lt;a href="https://github.com/codewithveek/drift-watch/blob/main/observability/signoz-dashboard.json" rel="noopener noreferrer"&gt;&lt;code&gt;observability/signoz-dashboard.json&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
via SigNoz &lt;strong&gt;Dashboards → New dashboard → Import JSON&lt;/strong&gt; — it contains all three panels below, pre-built.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9spb7onsxt6fcqn532r.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9spb7onsxt6fcqn532r.png" alt="A signoz dashboard showing driftwatch behaviour data" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another way to use &lt;strong&gt;DriftWatch&lt;/strong&gt; is through the &lt;code&gt;withSkillExecutionSpan&lt;/code&gt; function,&lt;br&gt;
Wrap a tool's &lt;code&gt;execute&lt;/code&gt; in &lt;code&gt;withSkillExecutionSpan&lt;/code&gt;. Every call then produces a&lt;br&gt;
&lt;code&gt;tool.&amp;lt;name&amp;gt;&lt;/code&gt; span and increments the &lt;code&gt;agent.tool.calls&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;agent.tool.duration&lt;/code&gt; metrics. These are the signals the drift detector reads.&lt;br&gt;
Use this for any tool, whether it's a database lookup, an HTTP call, or a vector search:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withSkillExecutionSpan&lt;/span&gt; &lt;span class="p"&gt;}&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;@driftwatch/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="p"&gt;}&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;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&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;get_repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;description&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 public information about a GitHub repository&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;withSkillExecutionSpan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;skillName&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_repo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;skillInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;executeSkill&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;res&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.github.com/repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&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="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;user-agent&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;driftwatch-demo&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;data&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;res&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stars&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;stargazers_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;language&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;language&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;h2&gt;
  
  
  What I learnt and challenges faced
&lt;/h2&gt;

&lt;p&gt;While building DriftWatch, I encounter series of challenges and bugs,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Telemetry silently going nowhere
While testing out DriftWatch, I had issues with my telemetry not showing up on the dashboard, and had to reach out to SigNoz's support through email, and &lt;strong&gt;Shivam&lt;/strong&gt;s response was truly helpful.
After which I realized that part of the problem was that I loaded my &lt;code&gt;.env&lt;/code&gt; file using &lt;code&gt;dotenv/config&lt;/code&gt; inside the application's entry point, which runs after the preload.
So when the preload tried to read &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt;, it got &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2.Traces worked, but my metrics disappeared&lt;br&gt;
After fixing the exporter, traces appeared almost immediately.&lt;br&gt;
but the custom metrics did not.&lt;/p&gt;

&lt;p&gt;Metrics such as:&lt;br&gt;
&lt;code&gt;agent.tokens&lt;/code&gt;,&lt;code&gt;agent.tool.calls&lt;/code&gt;,&lt;code&gt;agent.tool.duration&lt;/code&gt; were nowhere to be found, while runtime metrics like:&lt;br&gt;
&lt;code&gt;http.server.duration&lt;/code&gt; and &lt;code&gt;nodejs.eventloop&lt;/code&gt; showed up perfectly.&lt;/p&gt;

&lt;p&gt;This turned out to be a subtle OpenTelemetry JavaScript issue.&lt;/p&gt;

&lt;p&gt;Originally, I created the metric instruments at module load time.&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;const&lt;/span&gt; &lt;span class="nx"&gt;meter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;driftwatch&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;tokenCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent.tokens&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;p&gt;Unfortunately, that module was imported before my telemetry bootstrap registered the global &lt;code&gt;MeterProvider&lt;/code&gt;.&lt;br&gt;
When no provider exists, the Metrics API returns a no-op meter, so&lt;br&gt;
every instrument created from that meter silently drops measurements forever.&lt;/p&gt;

&lt;p&gt;The confusing part was that traces did not behave this way.&lt;/p&gt;

&lt;p&gt;The solution was to create instruments lazily.&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;let&lt;/span&gt; &lt;span class="nx"&gt;tokenCounter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;getTokenCounter&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="nx"&gt;tokenCounter&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt;
      &lt;span class="nx"&gt;metrics&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;driftwatch&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;createCounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent.tokens&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;The first request now creates the counter after the provider has already been registered.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calling the wrong Query Builder 
After fixing the telemetry flow, it was finally time to build the drift detector.
The detector queries SigNoz through &lt;code&gt;/api/v4/query_range&lt;/code&gt; but I was sending the older v3 query format:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;aggregateOperator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sum&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, the v4 expects aggregation to be split across separate fields:&lt;br&gt;
&lt;code&gt;temporality&lt;/code&gt;, &lt;code&gt;timeAggregation&lt;/code&gt;, and &lt;code&gt;spaceAggregation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once I updated my requests, counters started returning data.&lt;br&gt;
But there was still one final mystery.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agent.tool.duration&lt;/code&gt; always returned an empty result.&lt;/p&gt;

&lt;p&gt;The answer turned out to be hidden inside how SigNoz stores histograms.&lt;/p&gt;

&lt;p&gt;Instead of storing a histogram under one metric name, SigNoz expands it into several related series:&lt;br&gt;
&lt;code&gt;.bucket&lt;/code&gt; &lt;code&gt;.count&lt;/code&gt;, &lt;code&gt;.sum&lt;/code&gt;, &lt;code&gt;.min&lt;/code&gt;,&lt;code&gt;.max&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The base metric name itself is not directly queryable.&lt;/p&gt;

&lt;p&gt;To calculate percentiles, you query the &lt;code&gt;.bucket&lt;/code&gt; series instead.&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;"dataSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aggregateAttribute"&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent.tool.duration.bucket"&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;"Histogram"&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;"temporality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spaceAggregation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p95"&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;Lessons learnt&lt;/strong&gt;&lt;br&gt;
but at the end I was able to learn a number of new things, which includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deeper understanding of telemetry (this is my first time working with it), I now know what histograms and counters are, differences between &lt;code&gt;span&lt;/code&gt; and &lt;code&gt;meter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;How to create a dashboard using the UI or by importing a JSON.&lt;/li&gt;
&lt;li&gt;A better understanding of Vercel AI SDK&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;DriftWatch&lt;/strong&gt; SDK does more than what I talked about in this article, but I had to stop so as not to make it too long, Check out the documentation at &lt;a href="https://drift-watch-docs.vercel.app/docs/sdk" rel="noopener noreferrer"&gt;drift-watch-docs.vercel.app/docs/sdk&lt;/a&gt; for the full details about &lt;strong&gt;DriftWatch&lt;/strong&gt; capabilities, and other tools which I didn't mention in this post, including &lt;code&gt;@driftwatch/autopilot&lt;/code&gt; which lets you setup &lt;code&gt;scheduler&lt;/code&gt;s and &lt;code&gt;notifiers&lt;/code&gt; to get notify on Slack, Telegram or Webhook, whenever your agent drifts.&lt;/p&gt;

&lt;p&gt;Driftwatch is also open-source and contributions are welcomed on the &lt;a href="https://github.com/codewithveek/drift-watch" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>ai</category>
      <category>observability</category>
      <category>opentelemetry</category>
    </item>
    <item>
      <title>Detecting AI Agent Behavioral Drift with OpenTelemetry and SigNoz</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:35:39 +0000</pubDate>
      <link>https://dev.to/codewithveek/detecting-ai-agent-behavioral-drift-with-opentelemetry-and-signoz-3d98</link>
      <guid>https://dev.to/codewithveek/detecting-ai-agent-behavioral-drift-with-opentelemetry-and-signoz-3d98</guid>
      <description>&lt;p&gt;Recently, I have been working on AI agents and MCPs (I just completed one for a Fintech) &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe252cm0xw9xcvx5lr5n.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe252cm0xw9xcvx5lr5n.png" alt="A screenshot showing a financial expense graph in claude" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
and some of the challenges that kept showing up was, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how do I know what this agent is doing? &lt;/li&gt;
&lt;li&gt;how do I know how much token it consumed?&lt;/li&gt;
&lt;li&gt;what was the cost in USD?&lt;/li&gt;
&lt;li&gt;how do I know when this agent drifts from the intended action?&lt;/li&gt;
&lt;li&gt;and finally, how do I tell what the latency was, and when it spiked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While still thinking through on how to solve this problem, I came across this tweet on X&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd4adsk5f2d031ch8j49g.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd4adsk5f2d031ch8j49g.png" alt="A screenshot showing a post on X" width="720" height="1640"&gt;&lt;/a&gt;&lt;br&gt;
In the noon of that same day, I got an email from WeMakeDevs about the SigNoz hackathon, I went through the details and saw that it was about telemetry, and that was all the cue I needed.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the tool was born
&lt;/h2&gt;

&lt;p&gt;The frustration I encountered trying to understand the processes of these agents,such as wondering what changed, what model was used? what was the thought process of the model and the skill before it arrived at the previous result? was what led me to build DriftWatch&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;Introducing DriftWatch: An AI agent that &lt;strong&gt;observes itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So what is DriftWatch? &lt;br&gt;
DriftWatch is an AI agent SDK that &lt;strong&gt;observes itself&lt;/strong&gt;. Every skill (tool)&lt;br&gt;
call and LLM step is traced via OpenTelemetry into&lt;br&gt;
&lt;a href="https://signoz.io" rel="noopener noreferrer"&gt;SigNoz&lt;/a&gt;, and an AI layer on top of those traces flags&lt;br&gt;
&lt;strong&gt;behavioral drift&lt;/strong&gt; — shifts in tool-call mix, error rate, latency, or&lt;br&gt;
token spend between two time windows.&lt;/p&gt;

&lt;p&gt;DriftWatch does this by observing every process, tool calls, duration, drifts, as well as the model and/or skill used, and provides you with an Open telemetry records passed and analysed in SigNoz, which provides deep insights into the whats, whys and whens of every process.&lt;/p&gt;
&lt;h2&gt;
  
  
  How DriftWatch works
&lt;/h2&gt;

&lt;p&gt;DriftWatch wraps tool calls, and use open telemetry elements to track every process from initiation to response. Concretely, that means every agent run gets one parent &lt;code&gt;agent.run&lt;/code&gt; span, every tool call gets its own child span, and three custom metrics ride alongside them: &lt;code&gt;agent.tool.calls&lt;/code&gt; (counter, labelled by tool + outcome), &lt;code&gt;agent.tool.duration&lt;/code&gt; (histogram), and &lt;code&gt;agent.tokens&lt;/code&gt; (counter, labelled by model/provider/type). None of this is SigNoz-specific yet — it's just the OpenTelemetry Node SDK, instrumented once at process startup:&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;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&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;NodeSDK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resource&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;Resource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ATTR_SERVICE_NAME&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;telemetryConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent.kind&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;driftwatch&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;traceExporter&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;OTLPTraceExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&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;telemetryConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;otlpEndpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/traces`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;metricReader&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;PeriodicExportingMetricReader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;exporter&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;OTLPMetricExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;url&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;telemetryConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;otlpEndpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/metrics`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;exportIntervalMillis&lt;/span&gt;&lt;span class="p"&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="na"&gt;instrumentations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;getNodeAutoInstrumentations&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; at a self-hosted SigNoz collector (&lt;code&gt;http://localhost:4318&lt;/code&gt; if you're running SigNoz's own &lt;code&gt;docker compose up&lt;/code&gt; locally) and every span and metric above lands in SigNoz over OTLP/HTTP with zero extra glue code — this is the part that's genuinely "free": SigNoz's collector supports OTLP natively, so there's no translation layer to write.&lt;/p&gt;

&lt;p&gt;That gets you &lt;em&gt;visibility&lt;/em&gt;. &lt;br&gt;
But here's the part I actually wanted, noticing when an agent's behavior quietly changes, I needed something SigNoz doesn't do out of the box: comparing two time windows and asking whether the difference &lt;em&gt;means&lt;/em&gt; anything. So DriftWatch queries SigNoz's &lt;code&gt;/api/v4/query_range&lt;/code&gt; builder API directly for two one-hour windows (baseline vs. current) and diffs them:&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;const&lt;/span&gt; &lt;span class="nx"&gt;requestBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startTimeMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endTimeMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;compositeQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;queryType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;builder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panelType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;builderQueries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metrics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aggregateAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;agent.tool.calls&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;float64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="na"&gt;aggregateOperator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;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;tool&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;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;outcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metrics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aggregateAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;agent.tool.duration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;float64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="na"&gt;aggregateOperator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p95&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metrics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aggregateAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;agent.tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;float64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="na"&gt;aggregateOperator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;C&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="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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;signozBaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/query_range`&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGNOZ-API-KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signozApiKey&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;requestBody&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;Three builder queries in one request — tool-call counts (for tool mix and error rate), p95 tool latency, and total token spend — fanned out for both windows in parallel. The &lt;code&gt;SIGNOZ-API-KEY&lt;/code&gt; header comes from SigNoz's own UI (Settings → API Keys), and the builder query shape took a few failed requests to get right, since the v4 API's &lt;code&gt;aggregateAttribute&lt;/code&gt;/&lt;code&gt;dataType&lt;/code&gt; fields aren't obvious from the dashboard alone — I ended up reverse-engineering the exact payload by watching the network tab while building a panel manually in the SigNoz UI, then copying that shape into code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Screenshot: SigNoz dashboard showing the &lt;code&gt;agent.tool.calls&lt;/code&gt;, &lt;code&gt;agent.tool.duration&lt;/code&gt;, and &lt;code&gt;agent.tokens&lt;/code&gt; panels over a live run]&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once both windows come back as plain numbers (tool mix percentages, error rate, p95 latency, token spend), DriftWatch hands them to whatever model you've configured and asks it to judge the drift — with a strict JSON-only system prompt, retried up to a few times if the model wraps its answer in markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an SRE copilot that classifies whether an AI agent's behavior
has drifted enough to warrant a human alert. Reply with a SINGLE raw
JSON object: {"drift": bool, "severity": "none"|"low"|"medium"|"high",
"reasons": string[], "recommended_action": string}.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That verdict, plus the raw baseline/current numbers is what shows up as the &lt;code&gt;/drift&lt;/code&gt; endpoint response, and it's the same JSON the DriftWatch console renders as a human-readable card: severity, the specific metrics that moved, and a one-sentence recommendation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Screenshot: a drift verdict — e.g. "search_docs share went from 40% to 75%, error rate 2% → 9%", next to the SigNoz trace that shows the actual slow calls]&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Seeing it in SigNoz
&lt;/h2&gt;

&lt;p&gt;The whole loop is testable end-to-end without touching production traffic:&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="c"&gt;# bring up SigNoz locally&lt;/span&gt;
git clone https://github.com/SigNoz/signoz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;signoz/deploy/docker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# run DriftWatch pointed at it, then generate some load&lt;/span&gt;
git clone https://github.com/codewithveek/driftwatch &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;drift-watch &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm dev
&lt;span class="nv"&gt;BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3000 pnpm seed 40

&lt;span class="c"&gt;# pull a live drift verdict computed from SigNoz data&lt;/span&gt;
curl localhost:3000/drift
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pnpm seed 40&lt;/code&gt; fires 40 mixed requests through the agent, which is enough to populate both the baseline and current windows with real spans — open SigNoz at &lt;code&gt;localhost:8080&lt;/code&gt; right after and you can watch the &lt;code&gt;agent.run&lt;/code&gt; traces stack up, click into any one, and see every tool call as a child span with its own duration and status. That trace view is honestly the most useful part for debugging: when the drift judge flags "search_docs share 40% → 75%," you don't have to take its word for it, you click through to the actual traces from that window and see which tool calls dominated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[Screenshot: a single &lt;code&gt;agent.run&lt;/code&gt; trace expanded, showing child spans for each tool call with duration and &lt;code&gt;gen_ai.*&lt;/code&gt; attributes]&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Autopilot: closing the loop
&lt;/h2&gt;

&lt;p&gt;Once drift detection works, the obvious next question is "okay, now what"? so DriftWatch has a second, optional loop that turns a drift verdict into an action: pause the agent, roll it back to a known-good state, or just notify (Slack/Telegram/webhook), gated by policy rules and, for anything destructive, a human approval step. It's a thin layer on top of the same SigNoz-derived numbers, not a separate data source — I'm not going to detail it fully here since this post is about the SigNoz integration specifically, but it's what turns "we noticed the agent drifted" into something you can leave running unattended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;What worked: OpenTelemetry's auto-instrumentation for Fastify/HTTP meant I got baseline request tracing for free the moment &lt;code&gt;sdk.start()&lt;/code&gt; ran, before I'd written a single custom span. Layering &lt;code&gt;agent.run&lt;/code&gt;/tool spans and the three custom metrics on top of that was straightforward once I had the exporter pointed at SigNoz's collector correctly.&lt;/p&gt;

&lt;p&gt;What was hard: there's no existing semantic convention for "AI agent behavior" the way there is for HTTP or DB spans, so I had to invent my own attribute names (&lt;code&gt;agent.task_id&lt;/code&gt;, &lt;code&gt;agent.skills_used&lt;/code&gt;, &lt;code&gt;gen_ai.usage.*&lt;/code&gt;) and just be consistent about them. Getting SigNoz's v4 query builder payload shape right from code (rather than the UI) took real trial and error — the error messages from a malformed &lt;code&gt;compositeQuery&lt;/code&gt; aren't always specific about which field is wrong. And getting a model to reliably return bare JSON for the drift verdict was less reliable than I expected; the retry-with-correction-prompt approach was a hack that turned out to work well enough for a weekend build.&lt;/p&gt;

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

&lt;p&gt;DriftWatch turns SigNoz from "a place traces go to die" into an actual feedback loop for AI agents — instrument once with OpenTelemetry, query SigNoz's builder API for two time windows, and let a model tell you when something changed. Code: &lt;a href="https://github.com/codewithveek/drift-watch" rel="noopener noreferrer"&gt;https://github.com/codewithveek/drift-watch&lt;/a&gt; — live instance: &lt;a href="https://driftwatch.veek.me/console/" rel="noopener noreferrer"&gt;https://driftwatch.veek.me/console/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>otel</category>
      <category>signoz</category>
    </item>
    <item>
      <title>Build a Remittance App with the Afriex Cross-border API</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Tue, 30 Jun 2026 22:54:02 +0000</pubDate>
      <link>https://dev.to/afriex/build-a-remittance-app-with-the-afriex-business-api-1adg</link>
      <guid>https://dev.to/afriex/build-a-remittance-app-with-the-afriex-business-api-1adg</guid>
      <description>&lt;p&gt;Sending money across borders involves more moving parts than most developers expect. You need to know the live exchange rate before the user confirms. You need to register the recipient correctly. You need to attach their bank account or mobile wallet. You need to trigger the transfer and then track it through to settlement. And you need to handle failures gracefully when something goes wrong in the corridor.&lt;/p&gt;

&lt;p&gt;Most of that complexity is what the Afriex Cross-border API is built to handle. This article walks through the full remittance flow — from registering a sender to confirming delivery, using the Afriex SDK from start to finish.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get your API key
&lt;/h2&gt;

&lt;p&gt;Everything starts at &lt;a href="https://business.afriex.com" rel="noopener noreferrer"&gt;business.afriex.com&lt;/a&gt;. Create a Business account if you do not have one. Once you are in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Developer&lt;/strong&gt; then &lt;strong&gt;API Keys&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create new keys&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvvnd8m3nazk5vsnevlh6.png" alt="Afriex business dashboard developer tab" width="800" height="323"&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (e.g. "Remittance App")&lt;/li&gt;
&lt;li&gt;Set the permissions your use case needs — for a remittance app, you need at minimum: &lt;strong&gt;View wallet balances&lt;/strong&gt;, &lt;strong&gt;Initiate withdrawals&lt;/strong&gt;, &lt;strong&gt;View payment methods&lt;/strong&gt;, and &lt;strong&gt;Add or manage payment methods&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F26e6bba5sakudwvedub8.png" alt="Afriex business dashboard API permissions sheet" width="800" height="367"&gt;
&lt;/li&gt;
&lt;li&gt;Copy the key immediately. You cannot view it again after leaving the page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Store it as an environment variable:&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;AFRIEX_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-api-key-here
&lt;span class="nv"&gt;AFRIEX_ENVIRONMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging  &lt;span class="c"&gt;# switch to production when you go live&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start in the &lt;code&gt;staging&lt;/code&gt; environment so nothing real moves while you build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install and initialize the SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @afriex/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize once and reuse the instance across your application:&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="c1"&gt;// src/lib/afriex.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@afriex/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;afriex&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;Afriex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&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;AFRIEX_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&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;AFRIEX_ENVIRONMENT&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retryConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retryDelay&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;retryableStatusCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&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="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;webhookPublicKey&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;AFRIEX_WEBHOOK_PUBLIC_KEY&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 &lt;code&gt;retryConfig&lt;/code&gt; handles transient failures automatically. If the Afriex API returns a 429 or a 503, the SDK retries up to three times with a delay between attempts, so you do not have to write that logic yourself. The &lt;code&gt;webhookPublicKey&lt;/code&gt; is used later for signature verification, you can get it from the &lt;strong&gt;Developers &amp;gt; Webhooks&lt;/strong&gt; section of your dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Show the live exchange rate
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95ivqiv4kwaet3ltrwgb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95ivqiv4kwaet3ltrwgb.png" alt="showing Dollar to Naira exchange rate" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before anything else, show the sender what rate they are getting. A remittance user deciding between providers makes that decision based on the rate they see. Never show a hardcoded or cached rate — always fetch live.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRate&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&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;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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRates&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;fromSymbols&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="na"&gt;toSymbols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;to&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;rate&lt;/span&gt; &lt;span class="o"&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;rates&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;to&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;rate&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="s2"&gt;`Rate not available for &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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&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;return&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage: sender sends USD, recipient gets NGN&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rate&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;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&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="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;`1 USD = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; NGN`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This call returns the live mid-market rate for the currency pair. Show it to the user with a clear note about when it was fetched, since rates can shift between the moment they see it and the moment they confirm. The rate the user sees is informational — the actual conversion happens on Afriex's end when the transaction is created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the recipient as a customer
&lt;/h2&gt;

&lt;p&gt;The recipient needs to exist in the Afriex system before you can attach a payment method or send them money. This is a one-time step per recipient.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;fullName&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;email&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;phone&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;countryCode&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&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;details&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="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;details&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="c1"&gt;// E.164 format, e.g. +2348012345678&lt;/span&gt;
    &lt;span class="na"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ISO 3166-1 alpha-2, e.g. NG, KE, GH&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;customer&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;recipient&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;registerRecipient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Victory Lucky&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="s2"&gt;victory@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+2348012345678&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&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="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// store this — you need it in every subsequent call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;customerId&lt;/code&gt; that comes back. Every subsequent API call references this ID rather than the recipient's personal details.&lt;/p&gt;

&lt;p&gt;If you are building a product where senders send to the same recipient repeatedly (for example, a diaspora parent sending to family), store the &lt;code&gt;customerId&lt;/code&gt; against the recipient record in your own database on first creation and skip this step for future transfers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Attach the recipient's payment method
&lt;/h2&gt;

&lt;p&gt;Now tell Afriex where to send the money — the recipient's bank account or mobile wallet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bank account
&lt;/h3&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;attachBankAccount&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;customerId&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;accountName&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;accountNumber&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;institutionCode&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="c1"&gt;// bank code, e.g. "058" for GTBank Nigeria&lt;/span&gt;
  &lt;span class="nl"&gt;institutionName&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;countryCode&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentMethod&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;customerId&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BANK_ACCOUNT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accountName&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;accountName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accountNumber&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;accountNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;countryCode&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;countryCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;institution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;institutionCode&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;institutionCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;institutionName&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;institutionName&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;return&lt;/span&gt; &lt;span class="nx"&gt;paymentMethod&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;bankMethod&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;attachBankAccount&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customerId&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Victory Lucky&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0123456789&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;institutionCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;058&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;institutionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GTBank&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&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="nx"&gt;bankMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethodId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// store this too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mobile money
&lt;/h3&gt;

&lt;p&gt;For recipients receiving on MTN, M-Pesa, Airtel, or similar networks, the channel changes but the pattern is the same:&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;const&lt;/span&gt; &lt;span class="nx"&gt;mobileMethod&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customerId&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;channel&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&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Victory Lucky&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+2348012345678&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// recipient's mobile number&lt;/span&gt;
  &lt;span class="na"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;institution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;institutionCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MTN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;institutionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MTN Mobile Money&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;Not sure which institution code to use? Call &lt;code&gt;afriex.paymentMethods.listInstitutions({ countryCode: "NG", channel: "BANK_ACCOUNT" })&lt;/code&gt; to get the full list for any country and channel combination.&lt;/p&gt;

&lt;p&gt;Like the &lt;code&gt;customerId&lt;/code&gt;, save the &lt;code&gt;paymentMethodId&lt;/code&gt; that comes back. This is what you pass when you create the transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Send the transfer
&lt;/h2&gt;

&lt;p&gt;This is the call that actually moves money. Everything before this was set up, this is the execution.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/afriex&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendRemittance&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;customerId&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;paymentMethodId&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;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;sourceCurrency&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="c1"&gt;// currency the sender is sending&lt;/span&gt;
  &lt;span class="nl"&gt;destinationCurrency&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="c1"&gt;// currency the recipient receives&lt;/span&gt;
  &lt;span class="nl"&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="c1"&gt;// your internal reference for this transfer&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;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`idem-&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;reference&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;transaction&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WITHDRAW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customerId&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destinationId&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;paymentMethodId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceAmount&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;params&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="na"&gt;destinationAmount&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;params&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="na"&gt;sourceCurrency&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;sourceCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destinationCurrency&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;destinationCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&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;params&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;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;narration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Remittance — &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;reference&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;merchantId&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;reference&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;return&lt;/span&gt; &lt;span class="nx"&gt;transaction&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;transfer&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;sendRemittance&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customerId&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;paymentMethodId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bankMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethodId&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;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;destinationCurrency&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;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;txn-478-001&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="nx"&gt;transfer&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="c1"&gt;// the Afriex transaction ID — store this&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="nx"&gt;transfer&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="c1"&gt;// starts as PENDING&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth understanding about this call.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;idempotencyKey&lt;/code&gt; is derived from your own reference. If this call fails due to a network error and your code retries it, the same key means Afriex will not create a duplicate transaction. It recognizes the key and returns the existing transaction instead of creating a new one.&lt;/p&gt;

&lt;p&gt;The transaction comes back with a status of &lt;code&gt;PENDING&lt;/code&gt;. That is not a failure — it is the expected starting state. The actual settlement happens asynchronously, and Afriex tells you what happened next through webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Track the transfer through webhooks
&lt;/h2&gt;

&lt;p&gt;You will not know the outcome of a transfer by polling the response from the &lt;code&gt;create&lt;/code&gt; call. Afriex sends a signed HTTP POST to your webhook URL every time the transaction status changes. This is how you know when money has actually landed.&lt;/p&gt;

&lt;p&gt;First, register your webhook URL in the Afriex dashboard under &lt;strong&gt;Developers &amp;gt; Webhooks&lt;/strong&gt;. Copy the webhook public key from that page and add it to your environment:&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;AFRIEX_WEBHOOK_PUBLIC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-webhook-public-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use the built-in handler from the SDK:&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="c1"&gt;// Signature verification&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifySignature&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="kr"&gt;string&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="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyAndParse&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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 handler (Next.js route example)&lt;/span&gt;
&lt;span class="k"&gt;export&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;POST&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;Request&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;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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-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;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="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;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="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="s2"&gt;Missing signature&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;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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Read raw body before any parsing — the signature was computed against these exact bytes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="nf"&gt;verifySignature&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;rawBody&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;payload&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;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="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="s2"&gt;Invalid signature&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;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="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="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="s2"&gt;TRANSACTION.UPDATED&lt;/span&gt;&lt;span class="dl"&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="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="s2"&gt;TRANSACTION.CREATED&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="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="nx"&gt;status&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Update your database with the new status&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;transactionId&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="k"&gt;switch &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="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="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="c1"&gt;// Money has landed — notify the sender&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifySender&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your transfer was delivered.&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;IN_REVIEW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;// Compliance hold — not a failure, just needs time&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifySender&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your transfer is under review.&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;RETRY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;// The network is retrying automatically — no action needed&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="s2"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;REJECTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;// Terminal failure — let the sender know and offer a retry&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifySender&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your transfer could not be 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;break&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;return&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="na"&gt;received&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few important things about how this handler is written.&lt;/p&gt;

&lt;p&gt;It reads the raw body with &lt;code&gt;req.text()&lt;/code&gt; before parsing anything. The SDK handles the RSA-SHA256 signature verification which was computed against those exact &lt;code&gt;rawBody&lt;/code&gt; bytes. If you parse to JSON first and re-serialize, the verification will fail even if the payload is genuine.&lt;/p&gt;

&lt;p&gt;It returns &lt;code&gt;200&lt;/code&gt; immediately. Afriex expects a response within about five seconds. If your handler does slow work (database writes, email sending) before returning, put that work in a queue and return immediately. Afriex will retry delivery up to twelve times with exponential backoff if it does not get a success response.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IN_REVIEW&lt;/code&gt; and &lt;code&gt;RETRY&lt;/code&gt; are not failures. &lt;code&gt;IN_REVIEW&lt;/code&gt; means a compliance hold that will resolve into &lt;code&gt;SUCCESS&lt;/code&gt; or &lt;code&gt;REJECTED&lt;/code&gt;. &lt;code&gt;RETRY&lt;/code&gt; means the payment network is handling it automatically. Treating either as a failure will cause you to alert users unnecessarily.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full picture
&lt;/h2&gt;

&lt;p&gt;Here is the complete flow in sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Fetch live rate          →  afriex.rates.getRates()
2. Register recipient       →  afriex.customers.create()
3. Attach payment method    →  afriex.paymentMethods.create()
4. Send the transfer        →  afriex.transactions.create()
5. Receive status updates   →  afriex.webhooks.verifyAndParse()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steps 2 and 3 are one-time per recipient. For a sender who sends to the same person repeatedly, you store the &lt;code&gt;customerId&lt;/code&gt; and &lt;code&gt;paymentMethodId&lt;/code&gt; and jump straight to step 4 on subsequent transfers.&lt;/p&gt;

&lt;p&gt;Afriex handles the currency conversion, the corridor routing, and the settlement. Your application handles the user flow, the data storage, and the notification layer. The two concerns stay clean and separate.&lt;/p&gt;

&lt;p&gt;The full API reference is at &lt;a href="https://docs.afriex.com" rel="noopener noreferrer"&gt;docs.afriex.com&lt;/a&gt; and the Business dashboard is at &lt;a href="https://business.afriex.com" rel="noopener noreferrer"&gt;business.afriex.com&lt;/a&gt;. If you have questions, drop them in the comments or reach out on X &lt;a href="https://x.com/codewithveek" rel="noopener noreferrer"&gt;@codewithveek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>fintech</category>
      <category>crossborder</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Accept USDT and USDC Payments and Settle Automatically in USD with Afriex</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:16:12 +0000</pubDate>
      <link>https://dev.to/afriex/how-to-accept-usdt-and-usdc-payments-and-settle-automatically-in-usd-with-afriex-2m79</link>
      <guid>https://dev.to/afriex/how-to-accept-usdt-and-usdc-payments-and-settle-automatically-in-usd-with-afriex-2m79</guid>
      <description>&lt;p&gt;Stablecoin payments are becoming a mainstream way for businesses across Africa and global markets to collect funds. USDT and USDC now account for a significant share of cryptocurrency transaction volume because they offer fast settlement, lower transaction costs, and predictable value compared to traditional cross-border payment methods.&lt;/p&gt;

&lt;p&gt;For many businesses, however, accepting stablecoins introduces a new challenge: managing wallets, handling conversions, and moving funds into a usable currency.&lt;/p&gt;

&lt;p&gt;The Afriex crypto wallet endpoint simplifies this process. You generate a wallet address for USDT or USDC, your customer sends funds to that address, and Afriex automatically converts the deposit into its USD equivalent in your business balance.&lt;/p&gt;

&lt;p&gt;There is no manual conversion step, no separate swap operation, and no need to hold cryptocurrency after the payment is received.&lt;/p&gt;




&lt;h2&gt;
  
  
  Generate a USDT or USDC wallet address
&lt;/h2&gt;

&lt;p&gt;Getting started requires a single API call and one required parameter:&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;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCryptoWallet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or "USDC"&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;optional-customer-id&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;If you omit &lt;code&gt;customerId&lt;/code&gt;, the wallet belongs directly to your business account. This is useful when you want a single shared collection address for all incoming crypto payments.&lt;/p&gt;

&lt;p&gt;If you provide a &lt;code&gt;customerId&lt;/code&gt;, the wallet is associated with that specific customer. This approach is ideal when you need automatic payment attribution and want to know exactly which customer made a deposit without requiring payment references or manual reconciliation.&lt;/p&gt;

&lt;p&gt;The endpoint is idempotent. Requesting a wallet for the same asset and customer multiple times returns the same wallet address each time.&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;"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="s2"&gt;"695271a3ba52c13b669fad2b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"addresses"&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="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x1234567890123456789012345678901234567890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ETHEREUM_MAINNET"&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;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TYASr5UV6HEcXatwdFQfmLVUqQQQMUxHLS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRON_MAINNET"&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;For USDT wallets, Afriex returns addresses on both Ethereum and Tron networks. You can share whichever network your customer prefers. In most cases, Tron offers lower transaction fees and is often the more economical option for senders.&lt;/p&gt;




&lt;h2&gt;
  
  
  How crypto deposits are converted to USD
&lt;/h2&gt;

&lt;p&gt;Once USDT or USDC arrives at the generated wallet address, Afriex automatically converts the funds into their USD equivalent and records the transaction as a &lt;code&gt;DEPOSIT&lt;/code&gt; in your account.&lt;/p&gt;

&lt;p&gt;No additional API calls are required.&lt;/p&gt;

&lt;p&gt;The balance that appears in your business wallet is already denominated in USD, making it easier to manage accounting, payouts, and reporting without handling cryptocurrency directly.&lt;/p&gt;

&lt;p&gt;This workflow is especially useful for businesses that want to accept stablecoin payments while operating entirely in USD.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detecting successful crypto payments
&lt;/h2&gt;

&lt;p&gt;One important operational detail is that there is currently no dedicated webhook event for crypto-to-USD conversion deposits.&lt;/p&gt;

&lt;p&gt;If your application relies on real-time payment notifications, you should monitor incoming deposits by polling the transactions endpoint and looking for new &lt;code&gt;DEPOSIT&lt;/code&gt; records associated with the wallet's payment method.&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;const&lt;/span&gt; &lt;span class="nx"&gt;transactions&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DEPOSIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// filter or sort by createdAt to catch new ones&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common production approach is to poll periodically, track the latest processed transaction, and reconcile new deposits as they appear.&lt;/p&gt;

&lt;p&gt;This ensures your system can reliably identify successful payments and update customer records automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production requirements and limitations
&lt;/h2&gt;

&lt;p&gt;Like Afriex virtual accounts and pool accounts, crypto wallet collection is available only in the production environment.&lt;/p&gt;

&lt;p&gt;The endpoint is not available in staging or sandbox environments.&lt;/p&gt;

&lt;p&gt;During development, you should test your payment processing logic using mocked responses and validate the end-to-end deposit flow after going live.&lt;/p&gt;

&lt;p&gt;If you provide a &lt;code&gt;customerId&lt;/code&gt;, the associated customer must be verified. Requests made with an unverified customer return a &lt;code&gt;400&lt;/code&gt; validation error instead of a wallet address.&lt;/p&gt;




&lt;h2&gt;
  
  
  Crypto wallet vs virtual account
&lt;/h2&gt;

&lt;p&gt;Both crypto wallets and virtual accounts ultimately create attributable deposits in your Afriex account, but they solve different payment collection problems.&lt;/p&gt;

&lt;p&gt;Use a virtual account when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customers pay via local bank transfer&lt;/li&gt;
&lt;li&gt;Customers prefer traditional banking rails&lt;/li&gt;
&lt;li&gt;You are collecting fiat currency payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a crypto wallet when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customers already hold USDT or USDC&lt;/li&gt;
&lt;li&gt;You want to accept stablecoin payments&lt;/li&gt;
&lt;li&gt;You serve freelancers, remote workers, diaspora customers, or crypto-native businesses&lt;/li&gt;
&lt;li&gt;Customers prefer blockchain-based payment methods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Virtual accounts are covered in the virtual accounts guide.&lt;/p&gt;

&lt;p&gt;The Afriex crypto wallet endpoint is designed for businesses that want the speed and accessibility of stablecoin payments while keeping settlement and accounting simple through automatic USD conversion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Afriex support USDT on Tron?
&lt;/h3&gt;

&lt;p&gt;Yes. When you create a USDT wallet, Afriex returns wallet addresses for both the Ethereum and Tron networks.&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;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCryptoWallet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDT&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;You can provide either address to your customer depending on the network they intend to use. In many cases, Tron is preferred because transaction fees are typically lower than Ethereum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Afriex support USDC payments?
&lt;/h3&gt;

&lt;p&gt;Yes. The crypto wallet endpoint supports both USDT and USDC.&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;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCryptoWallet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once funds are received, Afriex automatically converts the deposit into its USD equivalent and credits your business balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How are crypto deposits settled?
&lt;/h3&gt;

&lt;p&gt;Crypto deposits are settled automatically in USD.&lt;/p&gt;

&lt;p&gt;When a customer sends USDT or USDC to a generated wallet address:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The blockchain confirms the transaction.&lt;/li&gt;
&lt;li&gt;Afriex receives the stablecoin deposit.&lt;/li&gt;
&lt;li&gt;The deposit is automatically converted into USD.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;DEPOSIT&lt;/code&gt; transaction is created in your account.&lt;/li&gt;
&lt;li&gt;Your USD balance is updated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No separate conversion request or manual swap operation is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I receive payment notifications through webhooks?
&lt;/h3&gt;

&lt;p&gt;Currently, Afriex does not emit a dedicated webhook event when a crypto deposit is converted into USD.&lt;/p&gt;

&lt;p&gt;To detect new crypto payments, periodically query the Transactions API and look for newly created &lt;code&gt;DEPOSIT&lt;/code&gt; transactions.&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;const&lt;/span&gt; &lt;span class="nx"&gt;transactions&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DEPOSIT&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;Many production systems poll this endpoint at regular intervals and reconcile newly discovered deposits automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I create separate wallets for different customers?
&lt;/h3&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;Pass a &lt;code&gt;customerId&lt;/code&gt; when creating a wallet to generate a customer-scoped payment destination.&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;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCryptoWallet&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USDT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customerId&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;id&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 makes payment attribution significantly easier because deposits can be associated directly with the customer who owns the wallet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the crypto wallet endpoint available in sandbox mode?
&lt;/h3&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;The crypto wallet endpoint is currently available only in production environments. During development, use mocked responses and test payment reconciliation workflows before validating against live deposits.&lt;/p&gt;

&lt;p&gt;Depending on how your customers prefer to pay, you may need a combination of crypto payment collection, bank transfers, and payout infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Afriex Virtual Accounts
&lt;/h3&gt;

&lt;p&gt;Use virtual accounts when customers need to pay via local bank transfer rather than cryptocurrency.&lt;/p&gt;

&lt;p&gt;Virtual accounts provide dedicated account details that make payment collection and reconciliation easier for fiat transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local bank transfers&lt;/li&gt;
&lt;li&gt;Customer deposits&lt;/li&gt;
&lt;li&gt;Marketplace payments&lt;/li&gt;
&lt;li&gt;Subscription collections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Read next: &lt;a href="https://dev.to/afriex/afriex-virtual-accounts-a-developers-guide-to-dedicated-and-pool-accounts-260b"&gt;Afriex Virtual Accounts: A Developer's Guide to Dedicated and Pool Accounts&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Afriex Pool Accounts
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.afriex.com/api-reference/endpoint/payment-methods/pool-account" rel="noopener noreferrer"&gt;Pool accounts&lt;/a&gt; are useful when you want to collect payments into a shared account and attribute funds programmatically after receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-volume payment collection&lt;/li&gt;
&lt;li&gt;Marketplaces&lt;/li&gt;
&lt;li&gt;Platforms with many users&lt;/li&gt;
&lt;li&gt;Automated reconciliation workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transactions API
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.afriex.com/api-reference/endpoint/transactions/create" rel="noopener noreferrer"&gt;Transactions API&lt;/a&gt; is particularly important when working with crypto deposits because it allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect successful deposits&lt;/li&gt;
&lt;li&gt;Reconcile incoming payments&lt;/li&gt;
&lt;li&gt;Build internal payment ledgers&lt;/li&gt;
&lt;li&gt;Trigger business workflows after settlement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you plan to accept USDT or USDC payments in production, this endpoint will likely become a core part of your integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customer Verification
&lt;/h3&gt;

&lt;p&gt;When creating customer-scoped wallets, ensure the associated customer has completed verification requirements.&lt;/p&gt;

&lt;p&gt;Unverified customers will receive validation errors when attempting to create crypto wallets tied to their account.&lt;/p&gt;

&lt;p&gt;Customer verification is therefore an important prerequisite for customer-specific payment collection workflows.&lt;/p&gt;




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

&lt;p&gt;The Afriex crypto wallet endpoint makes it easy to accept USDT and USDC payments without managing conversions yourself. Generate a wallet address, receive stablecoin payments, and have deposits automatically settled in USD.&lt;/p&gt;

&lt;p&gt;If you're ready to start accepting stablecoin payments, &lt;a href="https://business.afriex.com" rel="noopener noreferrer"&gt;create an Afriex account&lt;/a&gt;, complete your business setup, and begin collecting USDT and USDC with automatic USD settlement.&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>crypto</category>
      <category>crossborder</category>
      <category>payments</category>
    </item>
    <item>
      <title>Afriex Virtual Accounts: A Developer's Guide to Dedicated and Pool Accounts</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Sat, 20 Jun 2026 09:56:25 +0000</pubDate>
      <link>https://dev.to/afriex/afriex-virtual-accounts-a-developers-guide-to-dedicated-and-pool-accounts-260b</link>
      <guid>https://dev.to/afriex/afriex-virtual-accounts-a-developers-guide-to-dedicated-and-pool-accounts-260b</guid>
      <description>&lt;p&gt;Reconciling inbound payments seems straightforward until you have more than a handful of customers. Someone sends money to your business account, and now you need to determine who sent it, why they sent it, and what the payment was for. In most cases, this means asking customers to include a reference, which they will eventually forget.&lt;/p&gt;

&lt;p&gt;Virtual accounts solve this problem by reversing the model. Instead of having everyone pay into a single account, you generate a unique account number for a customer, a purpose, or even a single transaction. When funds arrive, you already know exactly who the payment belongs to. There is no reference matching, no guesswork, and no manual reconciliation.&lt;/p&gt;

&lt;p&gt;This guide explains how virtual accounts work in the Afriex Business API, including the two available account types, when to use each one, the key constraints you should understand before building, and the complete integration pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to collect: Dedicated accounts vs. pool accounts
&lt;/h2&gt;

&lt;p&gt;Afriex provides two distinct models for collecting inbound payments, and choosing the right one can significantly simplify your integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dedicated virtual accounts&lt;/strong&gt; (&lt;code&gt;VIRTUAL_BANK_ACCOUNT&lt;/code&gt;) are created specifically for an individual customer. You generate them through the API, and Afriex assigns a real account number linked directly to that customer in your system. Every deposit into that account is automatically attributed to the correct customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pool accounts&lt;/strong&gt; (&lt;code&gt;POOL_ACCOUNT&lt;/code&gt;) take a different approach. Afriex maintains a shared account that multiple customers can pay into. Deposits are reconciled using a &lt;code&gt;reference&lt;/code&gt; value that you store and track on your side. There is no account creation step. You simply call the endpoint, receive the account details and reference, and provide them to your customer.&lt;/p&gt;

&lt;p&gt;A simple rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;strong&gt;dedicated virtual account&lt;/strong&gt; when you want a stable account number permanently assigned to a customer.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;pool account&lt;/strong&gt; when you need a lightweight collection flow and are comfortable managing payment references yourself.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Important limitation: Production only
&lt;/h2&gt;

&lt;p&gt;Before implementing virtual accounts, be aware of one critical limitation: virtual accounts and pool accounts are only available in production environments.&lt;/p&gt;

&lt;p&gt;They are not supported in staging or sandbox environments. As a result, you cannot fully test the end-to-end collection flow before going live. The recommended approach is to build and unit test your integration against mocked responses, then carefully validate the complete payment flow after deployment to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dedicated virtual accounts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Static vs. dynamic accounts
&lt;/h3&gt;

&lt;p&gt;Dedicated virtual accounts come in two variants. The API determines which type to create based on the parameters you provide alongside &lt;code&gt;currency&lt;/code&gt; and &lt;code&gt;customerId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static accounts&lt;/strong&gt; are created with a &lt;code&gt;label&lt;/code&gt; and never expire. The account number remains valid indefinitely, making them ideal for customers who need to fund an account repeatedly over time.&lt;/p&gt;

&lt;p&gt;Supported labels include:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SALES&lt;/code&gt;, &lt;code&gt;OPERATIONS&lt;/code&gt;, &lt;code&gt;PAYROLL&lt;/code&gt;, &lt;code&gt;COLLECTIONS&lt;/code&gt;, &lt;code&gt;VENDOR_PAYMENTS&lt;/code&gt;, &lt;code&gt;TAX&lt;/code&gt;, &lt;code&gt;REFUNDS&lt;/code&gt;, &lt;code&gt;MARKETING&lt;/code&gt;, &lt;code&gt;TREASURY&lt;/code&gt;, and &lt;code&gt;GENERAL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The label is purely organizational. It allows you to categorize accounts internally according to their intended use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic accounts&lt;/strong&gt; are created using an &lt;code&gt;amount&lt;/code&gt; instead of a &lt;code&gt;label&lt;/code&gt;. They expire after a short period and are designed for collecting a specific amount within a limited timeframe, such as a one-time invoice or order payment.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;amount&lt;/code&gt; fields are mutually exclusive. Including both in the same request will result in an error.&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="c1"&gt;// Static virtual account, grouped under SALES&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staticAccount&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualAccount&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;68e6717848e1f632e9686460&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SALES&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;// Dynamic virtual account, tied to a specific amount&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamicAccount&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualAccount&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;68e6717848e1f632e9686460&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;50000&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 response includes the account details you can share directly with the customer:&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;"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;"paymentMethodId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"690cc5bbe2a1143ff6070119"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VIRTUAL_BANK_ACCOUNT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"68e6717848e1f632e9686460"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"institution"&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;"institutionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FIDELITY BANK"&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;"accountName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lily New"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accountNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3820404958"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&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;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;h3&gt;
  
  
  NGN virtual accounts and BVN requirements
&lt;/h3&gt;

&lt;p&gt;When creating a &lt;strong&gt;static NGN virtual account&lt;/strong&gt;, the customer must have a Bank Verification Number (BVN) on file. This is a regulatory requirement for permanently assigned Nigerian bank accounts.&lt;/p&gt;

&lt;p&gt;If the customer does not have a BVN recorded, account creation will fail.&lt;/p&gt;

&lt;p&gt;You can provide the BVN when creating the customer or update it later through the Customer KYC endpoint.&lt;/p&gt;

&lt;p&gt;This requirement applies only to static NGN virtual accounts. Dynamic virtual accounts are exempt, and the requirement does not apply to other currencies.&lt;/p&gt;

&lt;p&gt;If your product relies on static NGN accounts, collecting BVNs during onboarding will prevent failed account creation requests later in the user journey.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-customer account limits
&lt;/h3&gt;

&lt;p&gt;Virtual account creation is subject to limits based on the combination of business, customer, and currency.&lt;/p&gt;

&lt;p&gt;If you exceed the allowed number of accounts for a specific customer and currency, the API returns a &lt;code&gt;400&lt;/code&gt; response with the error code &lt;code&gt;VIRTUAL_ACCOUNT_LIMIT_REACHED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To avoid unnecessary creation attempts, consider checking for existing accounts before creating a new one:&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;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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listVirtualAccounts&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;68e6717848e1f632e9686460&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;if &lt;/span&gt;&lt;span class="p"&gt;(&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// safe to create&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVirtualAccount&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;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;68e6717848e1f632e9686460&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SALES&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;h3&gt;
  
  
  A breaking change to be aware of
&lt;/h3&gt;

&lt;p&gt;Previous versions of the API automatically created a virtual account when the list endpoint was called and no account existed.&lt;/p&gt;

&lt;p&gt;That behavior has been removed.&lt;/p&gt;

&lt;p&gt;The list endpoint is now strictly read-only. If no accounts exist, it returns a &lt;code&gt;200&lt;/code&gt; response with an empty array and does not create anything.&lt;/p&gt;

&lt;p&gt;Account creation must now be performed explicitly through the &lt;code&gt;POST&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;If your integration previously relied on a "get-or-create" workflow, update it to follow a clear check-then-create pattern like the example above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pool accounts
&lt;/h2&gt;

&lt;p&gt;Pool accounts operate differently because customers are not assigned dedicated account numbers.&lt;/p&gt;

&lt;p&gt;Instead, you call the endpoint with a country code, and Afriex returns an existing pool account along with a unique &lt;code&gt;reference&lt;/code&gt; value.&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;const&lt;/span&gt; &lt;span class="nx"&gt;poolAccount&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPoolAccount&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6929843e2c4653277440acc0&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;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;"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;"paymentMethodId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"69c2804b30314b491e48b305"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VIRTUAL_BANK_ACCOUNT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6929843e2c4653277440acc0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6929843e2c4653277440acc0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"institution"&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;"institutionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UBA"&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;"accountName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"accountNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&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;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;There are several important differences compared to dedicated virtual accounts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is no &lt;code&gt;currency&lt;/code&gt; parameter. Currency is inferred from the country code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NG&lt;/code&gt; automatically resolves to NGN.&lt;/li&gt;
&lt;li&gt;Pool accounts are only available in countries where Afriex has configured them.&lt;/li&gt;
&lt;li&gt;Requests for unsupported countries will return an error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important field in the response is &lt;code&gt;reference&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a &lt;code&gt;customerId&lt;/code&gt; is provided, the returned reference matches that customer. Store this value in your database before sharing account details with the customer.&lt;/p&gt;

&lt;p&gt;When a deposit arrives, Afriex uses the reference to identify the payment, allowing your system to correctly attribute funds to the appropriate customer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Dedicated Virtual Account&lt;/th&gt;
&lt;th&gt;Pool Account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Account number&lt;/td&gt;
&lt;td&gt;Unique per customer&lt;/td&gt;
&lt;td&gt;Shared across multiple customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creation required&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Recurring or long-term customer payments&lt;/td&gt;
&lt;td&gt;Quick collections and one-off payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer identification&lt;/td&gt;
&lt;td&gt;Automatic via account number&lt;/td&gt;
&lt;td&gt;Via stored reference value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supports repeated top-ups&lt;/td&gt;
&lt;td&gt;Yes (static accounts)&lt;/td&gt;
&lt;td&gt;Yes, but requires reference tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expires&lt;/td&gt;
&lt;td&gt;Static: No, Dynamic: Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BVN required for NGN&lt;/td&gt;
&lt;td&gt;Yes (static NGN accounts only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-customer account limits&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No customer-level creation limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal use cases&lt;/td&gt;
&lt;td&gt;Wallets, subscriptions, marketplaces, recurring billing&lt;/td&gt;
&lt;td&gt;Checkout payments, invoices, temporary collections&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Supported currencies
&lt;/h2&gt;

&lt;p&gt;Virtual accounts are currently available across four primary currencies: USD, NGN, GBP, and EUR.&lt;/p&gt;

&lt;p&gt;Current country coverage is shown below:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Currency&lt;/th&gt;
&lt;th&gt;Virtual Account Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nigeria&lt;/td&gt;
&lt;td&gt;NGN&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kenya&lt;/td&gt;
&lt;td&gt;KES&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;USD&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghana&lt;/td&gt;
&lt;td&gt;GHS&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United Kingdom, all EU countries&lt;/td&gt;
&lt;td&gt;GBP / EUR&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If virtual accounts are not yet available in your target market, plan for an alternative collection strategy. Depending on availability, that may mean using pool accounts or another payment method until dedicated virtual accounts become available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation pattern
&lt;/h2&gt;

&lt;p&gt;The typical lifecycle of a virtual account payment looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the customer in your system and register them with Afriex if necessary.&lt;/li&gt;
&lt;li&gt;Create a static or dynamic virtual account for that customer.&lt;/li&gt;
&lt;li&gt;Store the &lt;code&gt;paymentMethodId&lt;/code&gt; and &lt;code&gt;accountNumber&lt;/code&gt; in your database.&lt;/li&gt;
&lt;li&gt;Display the account details to the customer through your checkout flow, invoice, or payment instructions.&lt;/li&gt;
&lt;li&gt;The customer transfers funds to the virtual account.&lt;/li&gt;
&lt;li&gt;Afriex sends a deposit-related webhook event when funds arrive.&lt;/li&gt;
&lt;li&gt;Your webhook handler matches the event to the stored &lt;code&gt;paymentMethodId&lt;/code&gt; and updates the corresponding records.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This follows the same reconciliation philosophy described in the &lt;a href="https://dev.to/afriex/afriex-webhook-integration-guide-signature-verification-event-handling-and-production-best-3c9o"&gt;webhook integration guide&lt;/a&gt;. Webhooks should serve as the source of truth for payment events rather than relying on assumptions made by the application.&lt;/p&gt;

&lt;p&gt;For pool accounts, the process is similar, but the &lt;code&gt;reference&lt;/code&gt; becomes the primary identifier. Since multiple customers share the same account number, your webhook handler must use the stored reference to determine which customer a deposit belongs to.&lt;/p&gt;

&lt;p&gt;Without persisting that reference beforehand, accurate reconciliation becomes impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you use each option?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use a dedicated virtual account when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You have a known customer who will make recurring payments.&lt;/li&gt;
&lt;li&gt;You want deposits to be automatically attributed to that customer.&lt;/li&gt;
&lt;li&gt;You want to eliminate reference-based reconciliation entirely.&lt;/li&gt;
&lt;li&gt;You are building subscription products, marketplace wallets, stored balances, or recurring billing systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the &lt;strong&gt;dynamic&lt;/strong&gt; variant when collecting a specific amount within a limited time window, such as a single invoice or one-off purchase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a pool account when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need to start collecting payments quickly.&lt;/li&gt;
&lt;li&gt;You do not want to manage virtual account creation.&lt;/li&gt;
&lt;li&gt;You want to avoid customer-level account limits and BVN requirements.&lt;/li&gt;
&lt;li&gt;You are comfortable maintaining reference mappings in your own system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A checkout experience that generates a unique reference for each order while reusing the same account details for all customers is a common use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use an alternative payment method when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The country or currency you need is not yet supported.&lt;/li&gt;
&lt;li&gt;Virtual account coverage is still marked as "coming soon."&lt;/li&gt;
&lt;li&gt;Your target corridor requires a collection method that Afriex does not currently support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always verify availability before implementation using the &lt;a href="https://docs.afriex.com/guides/supported-currencies" rel="noopener noreferrer"&gt;supported currencies reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to see a practical implementation, the &lt;a href="https://dev.to/afriex/send-a-link-get-paid-building-payment-links-with-afriex-13ak"&gt;Afriex payment links guide&lt;/a&gt; demonstrates how to build shareable payment links backed by dynamic virtual accounts. For complete endpoint documentation and request schemas, refer to the &lt;a href="https://docs.afriex.com/api-reference/endpoint/payment-methods/virtual-account" rel="noopener noreferrer"&gt;Afriex API reference&lt;/a&gt; for virtual accounts and pool accounts.&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>fintech</category>
      <category>crossborder</category>
      <category>virtual</category>
    </item>
    <item>
      <title>What Afriex's Visa Direct Partnership Means for Users and Developers</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Wed, 17 Jun 2026 08:42:51 +0000</pubDate>
      <link>https://dev.to/afriex/what-afriexs-visa-direct-partnership-means-for-users-and-developers-bl2</link>
      <guid>https://dev.to/afriex/what-afriexs-visa-direct-partnership-means-for-users-and-developers-bl2</guid>
      <description>&lt;p&gt;On November 1, 2025, Afriex announced a partnership with Visa to enable real-time cross-border payments across more than 160 markets. Through an integration with Visa Direct, Afriex users can now send international money transfers through Visa's global payment network, potentially reducing delivery times from days to near real time.&lt;/p&gt;

&lt;p&gt;The integration, which is available immediately through the Afriex mobile app, represents one of the company's most significant upgrades to its cross-border payments infrastructure.&lt;/p&gt;

&lt;p&gt;But what does the Afriex Visa Direct partnership actually mean for users, businesses, and developers? More importantly, what has changed, and what remains unclear?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Visa Direct?
&lt;/h2&gt;

&lt;p&gt;Visa Direct is Visa's real-time money movement platform that enables funds to be pushed directly to eligible bank accounts, cards, and wallets connected to the Visa network.&lt;/p&gt;

&lt;p&gt;Unlike traditional correspondent banking systems, where international money transfers can pass through multiple intermediary banks before settlement, Visa Direct uses Visa's existing payment network and partner banking infrastructure to move funds more efficiently.&lt;/p&gt;

&lt;p&gt;For account and wallet payouts, Visa Direct leverages Visa's network alongside local ACH and real-time payment systems to deliver money into the recipient's domestic banking ecosystem. The result is faster settlement, improved transfer visibility, and a more streamlined payment experience.&lt;/p&gt;

&lt;p&gt;Visa Direct itself is not new. The service has been used by payment companies around the world for years. What is new is Afriex's integration with Visa Direct and its decision to incorporate the service into its growing remittance and cross-border payments platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Afriex's Visa Direct Integration Changes International Transfers
&lt;/h2&gt;

&lt;p&gt;Before the Visa Direct integration, most Afriex transfers relied on traditional banking rails that could take anywhere from one to several business days to settle, depending on the payment corridor and recipient bank.&lt;/p&gt;

&lt;p&gt;According to Afriex CEO Tope Alabi, speed and transparency remain critical for families and businesses that depend on remittance payments. By connecting to Visa Direct, Afriex can now route eligible transfers through a payment network designed for near real-time money movement.&lt;/p&gt;

&lt;p&gt;For users, the experience is straightforward. Funds can be sent through the Afriex mobile app and delivered to supported Visa-connected destinations much faster than traditional settlement methods allow.&lt;/p&gt;

&lt;p&gt;The scale of the opportunity is substantial. Visa Direct reaches more than 160 markets and connects to billions of accounts worldwide. However, that number reflects the size of Visa's global payment infrastructure rather than 160 new markets independently added by Afriex.&lt;/p&gt;

&lt;p&gt;In practical terms, the Afriex Visa Direct integration gives users access to a much larger network of potential payout destinations without requiring Afriex to build entirely new payment rails for every market.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Important Caveat
&lt;/h2&gt;

&lt;p&gt;The headline numbers do not mean every transfer automatically qualifies for Visa Direct.&lt;/p&gt;

&lt;p&gt;Availability depends on whether the recipient's financial institution participates in the relevant Visa Direct program and whether the destination account is eligible to receive Visa Direct payouts.&lt;/p&gt;

&lt;p&gt;If the recipient bank does not support the service, Afriex will continue using its existing payout methods, including bank account transfers, mobile money, SWIFT transfers, and other supported payment channels.&lt;/p&gt;

&lt;p&gt;There is another limitation worth noting. According to the partnership announcement, Afriex currently accesses Visa Direct through its financial institution partner, but Push-to-Wallet functionality is not yet commercially available. The live service today primarily supports push-to-account and push-to-card transfers rather than the full suite of Visa Direct products.&lt;/p&gt;

&lt;p&gt;This distinction matters because the integration should be viewed as an additional fast payment rail, not a complete replacement for Afriex's existing payment infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Afriex Is Expanding Its Cross-Border Payment Infrastructure
&lt;/h2&gt;

&lt;p&gt;The Visa Direct partnership fits neatly into Afriex's broader strategy of expanding global payment access while improving the efficiency of its cross-border payments network.&lt;/p&gt;

&lt;p&gt;In recent years, Afriex has expanded its reach across Europe, enabling senders in countries such as the Netherlands, Romania, Germany, Ireland, Italy, France, Belgium, and Spain to send money to African markets. The company has also invested in corridor infrastructure connecting African markets with countries including China, India, and Pakistan.&lt;/p&gt;

&lt;p&gt;The strategy is clear.&lt;/p&gt;

&lt;p&gt;Rather than building new payment infrastructure for every international corridor, Afriex can leverage established global networks such as Visa Direct where they provide meaningful advantages. At the same time, the company can continue investing in proprietary infrastructure where existing financial systems remain slow, expensive, or underserved.&lt;/p&gt;

&lt;p&gt;The move also aligns with broader industry trends. As global remittance flows continue to grow, users increasingly expect faster transfers, better visibility into payment status, and more reliable settlement times.&lt;/p&gt;

&lt;p&gt;For Afriex, integrating Visa Direct is a logical step toward meeting those expectations while strengthening its position in the international money transfer market.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Afriex Visa Direct Partnership Means for Developers
&lt;/h2&gt;

&lt;p&gt;For developers, the picture is less exciting, at least for now.&lt;/p&gt;

&lt;p&gt;As of this writing, there is no public indication that Visa Direct functionality is available through the Afriex Business API or SDK. Public announcements surrounding the Afriex Visa Direct partnership focus exclusively on the consumer experience within the Afriex mobile app.&lt;/p&gt;

&lt;p&gt;Developers using the Afriex Business API should continue working with the payment methods currently documented by Afriex, including bank accounts, mobile money, SWIFT transfers, and other supported disbursement channels.&lt;/p&gt;

&lt;p&gt;If Visa Direct capabilities are eventually exposed through the API, they would likely appear as an additional payment method alongside existing options such as &lt;code&gt;BANK_ACCOUNT&lt;/code&gt;, &lt;code&gt;MOBILE_MONEY&lt;/code&gt;, and &lt;code&gt;SWIFT&lt;/code&gt;. That expectation is based on Afriex's current API structure, but it remains speculation rather than documented functionality.&lt;/p&gt;

&lt;p&gt;Until official documentation confirms otherwise, developers should avoid designing workflows that depend on Visa Direct access through the Afriex Business API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The Afriex Visa Direct partnership represents a meaningful upgrade to the company's cross-border payments infrastructure. By connecting to Visa's global payment network, Afriex can offer faster international money transfers and improved payout speed across a large number of supported markets.&lt;/p&gt;

&lt;p&gt;For users, the benefits are immediate. Eligible transfers can move through Visa Direct's infrastructure and arrive significantly faster than they would through traditional banking channels.&lt;/p&gt;

&lt;p&gt;For developers, however, the story remains largely unchanged. Based on publicly available information, Visa Direct is currently a consumer-facing enhancement rather than a new API capability.&lt;/p&gt;

&lt;p&gt;The partnership is significant, but its biggest impact today is on transfer speed and user experience. Whether it eventually becomes part of the Afriex developer ecosystem remains a question for future product announcements.&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>crossborder</category>
      <category>visa</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Accept Payments in Minutes with Afriex Checkout Sessions</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Sat, 30 May 2026 16:48:17 +0000</pubDate>
      <link>https://dev.to/afriex/accept-payments-in-minutes-with-afriex-checkout-sessions-3j1m</link>
      <guid>https://dev.to/afriex/accept-payments-in-minutes-with-afriex-checkout-sessions-3j1m</guid>
      <description>&lt;p&gt;Most payment integrations follow the same painful path: you build a checkout UI, set up bank account display logic, handle Mobile Money prompts, manage session timers, poll for confirmation, and somehow do all of this correctly across every edge case. It takes days, sometimes weeks — and that's before you think about testing.&lt;/p&gt;

&lt;p&gt;Afriex Checkout Sessions changes that. With a single API call, you get a fully hosted payment page that handles everything: payment method selection, bank transfer details, Mobile Money authorization, session expiration, and redirect on completion. You stay focused on your product; Afriex handles the payment flow.&lt;/p&gt;

&lt;p&gt;This guide walks you through a complete integration from scratch — using either the official TypeScript SDK or the REST API directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The flow is four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your server&lt;/strong&gt; creates a checkout session via the SDK or REST API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Afriex&lt;/strong&gt; returns a &lt;code&gt;checkoutUrl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You redirect&lt;/strong&gt; the customer to that URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After payment&lt;/strong&gt;, Afriex redirects the customer back to your app and fires a webhook to your server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the entire integration surface. No payment UI to build. No polling loop. No bank account display to manage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;a href="https://business.afriex.com" rel="noopener noreferrer"&gt;Afriex Business account&lt;/a&gt; with API access&lt;/li&gt;
&lt;li&gt;Your API key from the &lt;a href="https://business.afriex.com/developer" rel="noopener noreferrer"&gt;Afriex dashboard&lt;/a&gt; (Developer tab)&lt;/li&gt;
&lt;li&gt;Node.js 20+ if you're using the SDK, or any server-side runtime for the REST API&lt;/li&gt;
&lt;li&gt;An HTTPS redirect URL where customers will land after payment&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sandbox first.&lt;/strong&gt; Checkout Sessions is currently available for end-to-end testing in Afriex's sandbox environment. Production rollout is happening soon — use sandbox to validate your integration now so you're ready to flip the switch.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using the SDK (Node.js / TypeScript)
&lt;/h3&gt;

&lt;p&gt;Install the official Afriex SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @afriex/sdk
&lt;span class="c"&gt;# or&lt;/span&gt;
pnpm add @afriex/sdk
&lt;span class="c"&gt;# or&lt;/span&gt;
yarn add @afriex/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then initialize it once — typically in a shared module or service file:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AfriexSDK&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@afriex/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;afriex&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;AfriexSDK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&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;AFRIEX_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or "production"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK maps &lt;code&gt;staging&lt;/code&gt; to &lt;code&gt;https://sandbox.api.afriex.com&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; to &lt;code&gt;https://api.afriex.com&lt;/code&gt; — no need to manage base URLs yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable retries for production resilience:&lt;/strong&gt;&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;afriex&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;AfriexSDK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&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;AFRIEX_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retryConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retryDelay&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="c1"&gt;// milliseconds&lt;/span&gt;
    &lt;span class="na"&gt;retryableStatusCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&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="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&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;h3&gt;
  
  
  Using the REST API directly
&lt;/h3&gt;

&lt;p&gt;Set your base URL depending on your environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sandbox:    https://sandbox.api.afriex.com
Production: https://api.afriex.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass your API key on every request via the &lt;code&gt;x-api-key&lt;/code&gt; header.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Checkout Session
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SDK (Node.js / TypeScript)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;afriex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./afriex-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your initialized SDK instance&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;createCheckoutSession&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;Order&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;session&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSession&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountInKobo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// e.g. 500000 for ₦5,000&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;merchantReference&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// must be unique per session&lt;/span&gt;
    &lt;span class="na"&gt;redirectUrl&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://yourapp.com/checkout/return&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;channels&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;VIRTUAL_BANK_ACCOUNT&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;MOBILE_MONEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;order&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="na"&gt;phone&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;customer&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="c1"&gt;// E.164 format: +2348100000000&lt;/span&gt;
      &lt;span class="na"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&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;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;orderId&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;plan&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;plan&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;return&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkoutUrl&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 SDK returns &lt;code&gt;{ checkoutUrl: string }&lt;/code&gt; — redirect your customer there immediately.&lt;/p&gt;




&lt;h3&gt;
  
  
  REST API
&lt;/h3&gt;

&lt;h4&gt;
  
  
  cURL
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://sandbox.api.afriex.com/api/v1/checkout-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "amount": 500000,
    "currency": "NGN",
    "merchantReference": "order-2026-05-30-001",
    "redirectUrl": "https://yourapp.com/checkout/return",
    "channels": ["VIRTUAL_BANK_ACCOUNT", "MOBILE_MONEY"],
    "customer": {
      "name": "Amarachi Okafor",
      "email": "amara@example.com",
      "phone": "+2348100000000",
      "countryCode": "NG"
    },
    "metadata": {
      "orderId": "ORD-9912",
      "plan": "pro"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Node.js (fetch)
&lt;/h4&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;createCheckoutSession&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="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="s2"&gt;https://sandbox.api.afriex.com/api/v1/checkout-session&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="s2"&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="s2"&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="s2"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&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;AFRIEX_API_KEY&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="na"&gt;amount&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;amountInKobo&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;merchantReference&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;redirectUrl&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://yourapp.com/checkout/return&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;channels&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;VIRTUAL_BANK_ACCOUNT&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;MOBILE_MONEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;order&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="na"&gt;phone&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;customer&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;countryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&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;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;orderId&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;id&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="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;data&lt;/span&gt; &lt;span class="p"&gt;}&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkoutUrl&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;h4&gt;
  
  
  Python (requests)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_checkout_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://sandbox.api.afriex.com/api/v1/checkout-session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AFRIEX_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount_in_kobo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NGN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merchantReference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirectUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://yourapp.com/checkout/return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VIRTUAL_BANK_ACCOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MOBILE_MONEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countryCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NG&lt;/span&gt;&lt;span class="sh"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkoutUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Response
&lt;/h4&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;"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;"checkoutUrl"&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://sandbox.pay.afriex.com/pay/eyJhbGciOiJI..."&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;Redirect your customer to &lt;code&gt;checkoutUrl&lt;/code&gt; immediately. The session has an expiration timer, so don't hold on to the URL for later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Request Reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Required Fields
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;amount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;integer&lt;/td&gt;
&lt;td&gt;Amount in &lt;strong&gt;minor currency units&lt;/strong&gt; (kobo for NGN, cents for USD). Minimum: &lt;code&gt;100&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;currency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;ISO 4217 currency code, uppercase (e.g. &lt;code&gt;NGN&lt;/code&gt;, &lt;code&gt;GHS&lt;/code&gt;). Must be enabled for checkout on your account.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;merchantReference&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Your unique identifier for this session. Used to match webhooks and reconcile transactions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redirectUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;HTTPS URL the customer is sent to after the checkout flow completes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;Customer details. See below.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Customer Object
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Customer's full name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Customer's email address.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;phone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Phone number in E.164 format (e.g. &lt;code&gt;+2348100000000&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;countryCode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;ISO 3166-1 alpha-2 country code (e.g. &lt;code&gt;NG&lt;/code&gt;, &lt;code&gt;GH&lt;/code&gt;). Case-insensitive.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Optional Fields
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;channels&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;&lt;code&gt;["VIRTUAL_BANK_ACCOUNT"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment methods to offer. Options: &lt;code&gt;VIRTUAL_BANK_ACCOUNT&lt;/code&gt;, &lt;code&gt;MOBILE_MONEY&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Flat key/value pairs (strings only) you want attached to the session for your own reference.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 2: The Customer Completes Payment
&lt;/h2&gt;

&lt;p&gt;Once redirected, the customer lands on the Afriex-hosted checkout page. Depending on the &lt;code&gt;channels&lt;/code&gt; you enabled, they can:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pay by Bank Transfer
&lt;/h3&gt;

&lt;p&gt;The customer sees a one-time virtual bank account — account name, number, and bank — with the exact amount to transfer. They open their banking app, send the transfer, and return to the checkout page. Afriex detects the inbound transfer and confirms payment automatically.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Exact amount required.&lt;/strong&gt; The customer must transfer the precise amount shown. Sending a different amount will cause delays and may require a manual reconciliation or refund.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Pay by Mobile Money
&lt;/h3&gt;

&lt;p&gt;The customer selects their network (e.g. MTN, Airtel), enters their account name and phone number, and taps &lt;strong&gt;Pay now&lt;/strong&gt;. Afriex pushes an authorization prompt to their phone. Once they approve it, the checkout page updates automatically.&lt;/p&gt;

&lt;p&gt;Both paths end in one of two outcomes: a &lt;strong&gt;success screen&lt;/strong&gt; (auto-redirects to your &lt;code&gt;redirectUrl&lt;/code&gt;) or a &lt;strong&gt;failure screen&lt;/strong&gt; (with guidance to retry or contact support).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Handle the Redirect
&lt;/h2&gt;

&lt;p&gt;When the customer lands back on your &lt;code&gt;redirectUrl&lt;/code&gt;, show them an appropriate confirmation or error screen. Use the &lt;code&gt;merchantReference&lt;/code&gt; you generated to look up the order state in your system.&lt;/p&gt;

&lt;p&gt;A typical return handler:&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;// Express.js example&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/checkout/return&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;// The merchantReference you passed when creating the session&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reference&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;query&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="c1"&gt;// Look up your order by reference — don't trust query params for payment status&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByReference&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;if &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="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/orders/&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;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/confirmation`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Not yet confirmed — webhook may still be in flight&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;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/orders/&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;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pending`&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;&lt;strong&gt;Don't use the redirect to confirm payment.&lt;/strong&gt; The redirect tells you the customer finished the checkout flow, not that payment succeeded. Always use the webhook (see Step 4) as the authoritative signal.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: Listen for the Webhook
&lt;/h2&gt;

&lt;p&gt;When Afriex confirms payment, it fires a &lt;code&gt;CHECKOUT_SESSION.CREATED&lt;/code&gt; webhook to the callback URL configured in your Afriex dashboard. This is the source of truth for marking an order as 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;// Express.js webhook handler example&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="s2"&gt;/webhooks/afriex&lt;/span&gt;&lt;span class="dl"&gt;"&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;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&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;parse&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;if &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;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CHECKOUT_SESSION.CREATED&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merchantReference&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="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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Mark order as paid using your merchantReference&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;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markPaid&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;merchantReference&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="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="s2"&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;Make your webhook handler idempotent — Afriex may retry delivery if your endpoint doesn't respond with a &lt;code&gt;200&lt;/code&gt;. Using &lt;code&gt;merchantReference&lt;/code&gt; as your idempotency key is the right approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond the Basic Flow: Advanced Use Cases
&lt;/h2&gt;

&lt;p&gt;The checkout session API supports a few patterns beyond the standard redirect flow that are worth knowing about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delayed Payment Collection
&lt;/h3&gt;

&lt;p&gt;You don't have to redirect customers immediately after creating a session. You can create the session on your server, store the &lt;code&gt;checkoutUrl&lt;/code&gt;, and send it to the customer later — via email, SMS, or a payment link in an invoice. This is useful for billing flows where payment isn't expected at the point of order.&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="c1"&gt;// Create the session when the invoice is generated&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;afriex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;invoiceDetails&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Store checkoutUrl against the invoice&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;invoices&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;invoice&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="na"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkoutUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Send it later when the invoice is dispatched&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailer&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="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&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="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your invoice is ready&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;`Pay here: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentUrl&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;Keep session expiration in mind — if the payment link will be sent days later, build in a check for whether the session is still valid before sending.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedded Checkout (Iframe / Webview)
&lt;/h3&gt;

&lt;p&gt;Rather than fully leaving your app, you can open the &lt;code&gt;checkoutUrl&lt;/code&gt; inside an iframe (web) or a webview (mobile). This keeps the customer inside your product's shell while Afriex handles the payment UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Web: iframe embed --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://sandbox.pay.afriex.com/pay/YOUR_SESSION_TOKEN"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt;
  &lt;span class="na"&gt;frameborder=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;allow=&lt;/span&gt;&lt;span class="s"&gt;"payment"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React Native: WebView embed&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WebView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native-webview&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebView&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkoutUrl&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="nx"&gt;onNavigationStateChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;state&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;// Detect redirect back to your redirectUrl&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&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://yourapp.com/checkout/return&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;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OrderConfirmation&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook still fires regardless of how the customer accesses the checkout — embedded or full redirect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Amounts are always in minor units
&lt;/h3&gt;

&lt;p&gt;Afriex uses the smallest denomination of each currency — no decimals in the &lt;code&gt;amount&lt;/code&gt; field:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Currency&lt;/th&gt;
&lt;th&gt;Major unit&lt;/th&gt;
&lt;th&gt;Minor unit&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NGN&lt;/td&gt;
&lt;td&gt;₦1&lt;/td&gt;
&lt;td&gt;1 kobo&lt;/td&gt;
&lt;td&gt;₦5,000 → &lt;code&gt;500000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GHS&lt;/td&gt;
&lt;td&gt;₵1&lt;/td&gt;
&lt;td&gt;1 pesewa&lt;/td&gt;
&lt;td&gt;₵100 → &lt;code&gt;10000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USD&lt;/td&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;td&gt;1 cent&lt;/td&gt;
&lt;td&gt;$10 → &lt;code&gt;1000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The minimum &lt;code&gt;amount&lt;/code&gt; is &lt;code&gt;100&lt;/code&gt; (equivalent to 1 major unit).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;merchantReference&lt;/code&gt; must be unique
&lt;/h3&gt;

&lt;p&gt;Each session needs a distinct &lt;code&gt;merchantReference&lt;/code&gt;. Re-using a reference from a previous session will result in an error. Use your internal order ID, a UUID, or any other identifier you control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sessions expire
&lt;/h3&gt;

&lt;p&gt;Each checkout session has an expiration timer visible to the customer. If the session expires before payment completes, the customer will need a new session. Build a recovery path — for example, let customers re-initiate checkout from your order page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phone numbers must be E.164
&lt;/h3&gt;

&lt;p&gt;The customer's &lt;code&gt;phone&lt;/code&gt; must be in E.164 format: &lt;code&gt;+&lt;/code&gt; followed by the country code and subscriber number, no spaces or punctuation. For example, a Nigerian number would be &lt;code&gt;+2348100000000&lt;/code&gt;, not &lt;code&gt;08100000000&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing in Sandbox
&lt;/h2&gt;

&lt;p&gt;All of the above works end-to-end in sandbox today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use base URL &lt;code&gt;https://sandbox.api.afriex.com&lt;/code&gt; (or set &lt;code&gt;environment: "staging"&lt;/code&gt; in the SDK)&lt;/li&gt;
&lt;li&gt;Use your sandbox API key from the Afriex dashboard&lt;/li&gt;
&lt;li&gt;Successful checkout sessions redirect to &lt;code&gt;https://sandbox.pay.afriex.com/pay/{token}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Webhooks fire to your configured callback URL in sandbox as well&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Test the full round-trip — create a session, go through the hosted checkout, and verify your webhook handler receives and processes the event — before switching to production credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error Responses
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invalid request — check required fields, amount minimum, and currency format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unauthorized — verify your &lt;code&gt;x-api-key&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server error — retry with exponential backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://afriex-sdk-docs.vercel.app/getting-started" rel="noopener noreferrer"&gt;Afriex SDK Documentation&lt;/a&gt;&lt;/strong&gt; — Full SDK reference covering installation, configuration, and all available modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://afriex-sdk-docs.vercel.app/api/checkout" rel="noopener noreferrer"&gt;SDK Checkout Reference&lt;/a&gt;&lt;/strong&gt; — SDK-specific parameters and examples for the Checkout API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.afriex.com/api-reference/endpoint/checkout-sessions/user-flow-guide" rel="noopener noreferrer"&gt;User Flow Guide&lt;/a&gt;&lt;/strong&gt; — See the full customer-facing checkout experience, screen by screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.afriex.com/api-reference/endpoint/webhooks/introduction" rel="noopener noreferrer"&gt;Webhooks&lt;/a&gt;&lt;/strong&gt; — Configure your callback URL and understand the full webhook payload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.afriex.com/api-reference/endpoint/checkout-sessions/create" rel="noopener noreferrer"&gt;REST API Reference&lt;/a&gt;&lt;/strong&gt; — Full OpenAPI spec for the Create Checkout Session endpoint&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Afriex Checkout Sessions is currently available in sandbox. Production rollout is expected in the coming weeks — get your integration ready today.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>stripe</category>
      <category>fintech</category>
      <category>crossborder</category>
    </item>
    <item>
      <title>Afriex vs Flutterwave vs Paystack: Which One Should You Choose?</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Mon, 25 May 2026 12:56:13 +0000</pubDate>
      <link>https://dev.to/afriex/afriex-vs-flutterwave-vs-paystack-which-one-should-you-use-for-payouts-and-disbursements-20jc</link>
      <guid>https://dev.to/afriex/afriex-vs-flutterwave-vs-paystack-which-one-should-you-use-for-payouts-and-disbursements-20jc</guid>
      <description>&lt;p&gt;If you are building something that collects payments from customers, Paystack and Flutterwave are both solid choices and you probably already know that. This article is not about that.&lt;/p&gt;

&lt;p&gt;This is about the other direction: sending money out. Paying a freelancer in Nairobi from a USD balance. Disbursing contractor fees to someone in Accra via MTN Mobile Money. Running payroll across five African countries without building a separate integration for each one. That is a different problem, and it needs a different tool.&lt;/p&gt;

&lt;p&gt;The honest answer is that Paystack and Flutterwave were built collection-first. They handle disbursements, but with limits that matter once you are operating across borders. Afriex was built disbursement-first, for African corridors specifically. The comparison is not that one is better overall — it is that they solve different problems, and picking the wrong one for outbound payments means hitting walls you did not expect.&lt;/p&gt;

&lt;p&gt;Here is how they actually compare.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we are comparing
&lt;/h2&gt;

&lt;p&gt;Three things matter for outbound disbursement:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage&lt;/strong&gt; — which countries and payment channels can you actually pay out to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer experience&lt;/strong&gt; — how the API is structured, what you need to do before money can move, how status updates work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fit for the use case&lt;/strong&gt; — whether the tool was designed for this job or adapted to it.&lt;/p&gt;

&lt;p&gt;We are not comparing collection features, pricing pages, or dashboard UX. Those are separate decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coverage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Paystack
&lt;/h3&gt;

&lt;p&gt;Paystack is currently available for companies based in Nigeria, South Africa, Ghana, Kenya, and Côte d'Ivoire, supporting NGN, GHS, ZAR, KES, and USD (USD only in Kenya and Nigeria).&lt;/p&gt;

&lt;p&gt;For outbound transfers, Paystack supports bank account payouts within those same markets. It primarily settles in local currencies — if someone pays in USD via a foreign card, funds are often converted to NGN or another local currency. USD payout to a recipient abroad is not a native flow.&lt;/p&gt;

&lt;p&gt;If your recipients are Nigerian businesses with local NGN bank accounts and you are already using Paystack for collections, the transfers API works and settlement is fast. The moment your disbursement needs go outside Nigeria, Ghana, Kenya, or South Africa — or you need to pay in USD without conversion — Paystack starts to show its limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flutterwave
&lt;/h3&gt;

&lt;p&gt;Flutterwave is supported in Nigeria, Ghana, Kenya, South Africa, Uganda, Tanzania, the United Kingdom, the United States, and Europe. It processes payouts in 30+ currencies and has broader mobile money coverage than Paystack across East and West Africa.&lt;/p&gt;

&lt;p&gt;Flutterwave is the wider net of the two, and for businesses that need to pay out in markets like Uganda and Tanzania where Paystack does not operate, it is the more practical choice. The transfers API is well-documented and the mobile money coverage is real.&lt;/p&gt;

&lt;p&gt;The gap is that Flutterwave is still fundamentally a collection platform that also does transfers. The payout flow works, but the API was not designed with multi-country disbursement as its primary job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Afriex
&lt;/h3&gt;

&lt;p&gt;Afriex covers &lt;a href="https://docs.afriex.com/api-reference/supported-currencies" rel="noopener noreferrer"&gt;31 African countries&lt;/a&gt; with payout rails across bank accounts, mobile money (MTN MoMo, M-Pesa, Airtel Money, and others), and SWIFT. Beyond Africa, USD SWIFT payouts reach 100 countries globally, including the US, UK, all of Europe, India, China, Canada, and more. Payment channels include bank transfers, mobile money, SWIFT, UPI (India), Interac (Canada), WeChat Pay and Alipay (China), and crypto (USDC/USDT).&lt;/p&gt;

&lt;p&gt;The coverage table for African countries specifically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Payout Rails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nigeria&lt;/td&gt;
&lt;td&gt;Bank Account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kenya&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money, Paybill/Till&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghana&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uganda&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tanzania&lt;/td&gt;
&lt;td&gt;Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cameroon&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Côte d'Ivoire&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethiopia&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egypt&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;South Africa&lt;/td&gt;
&lt;td&gt;Bank Account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rwanda&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senegal&lt;/td&gt;
&lt;td&gt;Bank Account, Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ 19 more African countries&lt;/td&gt;
&lt;td&gt;Mobile Money&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference is not just the number of countries. It is that mobile money is a first-class channel in the Afriex API, not an add-on. In Kenya, you can pay to a bank account, an M-Pesa wallet, or a Paybill/Till number from the same integration. In Ghana, bank and MTN MoMo are both available. For many recipients in these markets, mobile money is their primary financial account, not a fallback.&lt;/p&gt;




&lt;h2&gt;
  
  
  Developer experience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How Paystack transfers work
&lt;/h3&gt;

&lt;p&gt;Paystack's transfers API follows a straightforward pattern: create a transfer recipient (stores the bank account details), then initiate a transfer to that recipient. The API is clean and the docs are good. The limitation is that it is scoped to Paystack's supported markets, so there is no concept of channel selection — bank account is the primary output.&lt;/p&gt;

&lt;p&gt;For a developer building payroll or contractor payouts strictly within Nigeria, the API is simple and it works. For anything involving mobile money payouts or recipients outside Paystack's five markets, you are building a separate integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Flutterwave transfers work
&lt;/h3&gt;

&lt;p&gt;Flutterwave's transfer API accepts bank account details directly without a separate recipient creation step, which makes it slightly lighter for one-off payouts. Mobile money transfers are supported for several African markets. The documentation covers the major corridors well.&lt;/p&gt;

&lt;p&gt;The gap developers hit in production is reliability. Flutterwave offers broad African coverage across 34 countries with extensive mobile money integration but has more variable API reliability than Paystack or Stripe. For a payroll system where every transfer matters, variable reliability in production is a real constraint to account for.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Afriex transfers work
&lt;/h3&gt;

&lt;p&gt;The Afriex API uses a three-step model: create a customer, attach a payment method, create a transaction. It is slightly more upfront setup than Flutterwave's direct transfer approach, but the structure is deliberate — the payment method record stores verified account details that can be reused across multiple payouts without re-entering them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/v1/customer       → returns customerId
POST /api/v1/payment-method → returns paymentMethodId  
POST /api/v1/transaction    → uses both IDs, returns transactionId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a payroll use case, this is actually the right model. You register an employee once, attach their bank account or mobile wallet once, and then every subsequent payout is a single transaction call with the stored IDs. Repeat disbursements to the same recipient require no additional setup.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;meta.idempotencyKey&lt;/code&gt; field on every transaction prevents duplicates if a job retries after a network failure. The &lt;code&gt;reference&lt;/code&gt; field maps every transaction back to your internal records. Both are required, which enforces good practice.&lt;/p&gt;

&lt;p&gt;Transaction types map cleanly to the three outbound flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WITHDRAW&lt;/code&gt; — send money from your Afriex wallet to a recipient's account&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEPOSIT&lt;/code&gt; — pull money from a customer's account into your wallet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SWAP&lt;/code&gt; — convert between currencies in your Afriex wallet without touching any external account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For disbursement specifically, &lt;code&gt;WITHDRAW&lt;/code&gt; is the one you use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Status updates and webhooks
&lt;/h2&gt;

&lt;p&gt;This is where the tools diverge most practically for anyone building a reliable disbursement system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paystack and Flutterwave
&lt;/h3&gt;

&lt;p&gt;Both support webhooks for transfer status updates. The status vocabulary is standard: pending, success, failed. For domestic transfers that settle quickly, this is sufficient. For cross-border payouts that can sit in intermediate states for longer — especially in corridors with compliance review steps — a simple success/failed model leaves you with less visibility than you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Afriex
&lt;/h3&gt;

&lt;p&gt;The Afriex webhook system fires on &lt;code&gt;TRANSACTION.CREATED&lt;/code&gt; and &lt;code&gt;TRANSACTION.UPDATED&lt;/code&gt;. &lt;br&gt;
Here's the full status vocabulary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;What it means for your system&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PENDING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Received, not yet processed — do nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROCESSING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Actively moving — do nothing, wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;COMPLETED&lt;/code&gt; / &lt;code&gt;SUCCESS&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Settled — notify recipient, update your records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN_REVIEW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Under compliance review — notify the user, do not mark as failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RETRY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network is retrying automatically — do nothing&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;Terminal failure — alert user, allow retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REJECTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejected after review — alert user with reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REFUNDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Funds returned — update your records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNKNOWN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Indeterminate — alert your team to investigate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;IN_REVIEW&lt;/code&gt; and &lt;code&gt;RETRY&lt;/code&gt; statuses are the ones that matter most for cross-border corridors. &lt;code&gt;IN_REVIEW&lt;/code&gt; reflects a real compliance hold that African payment networks produce — it is not a failure state, and treating it as one causes support noise for payouts that are actually in progress. &lt;code&gt;RETRY&lt;/code&gt; means the network is handling it without any action needed from your code.&lt;/p&gt;

&lt;p&gt;Webhooks are signed with RSA-SHA256 and verified against your public key from the dashboard. Afriex retries delivery up to 12 times with exponential backoff from 30 seconds to 16 hours, which means a temporary outage on your end does not mean lost status updates.&lt;/p&gt;

&lt;p&gt;The full webhook implementation guide is covered in the &lt;a href="https://dev.to/afriex/how-to-handle-afriex-webhooks-the-right-way"&gt;Afriex webhook article&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use each one
&lt;/h2&gt;

&lt;p&gt;This is the honest summary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Paystack transfers if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your recipients are in Nigeria, Ghana, Kenya, South Africa, or Côte d'Ivoire only&lt;/li&gt;
&lt;li&gt;You are already using Paystack for collection and want a single integration&lt;/li&gt;
&lt;li&gt;Your payout volume is primarily NGN bank transfers with fast local settlement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Flutterwave transfers if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to pay out in markets Paystack does not cover, like Uganda or Tanzania&lt;/li&gt;
&lt;li&gt;You need mobile money coverage across multiple East and West African markets&lt;/li&gt;
&lt;li&gt;Your use case tolerates some variability in API reliability for the sake of broader coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Afriex if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are building a product where disbursement is the primary job: payroll, contractor payouts, scholarship disbursements, creator payouts&lt;/li&gt;
&lt;li&gt;Your recipients are spread across African countries and corridors, including mobile money wallets&lt;/li&gt;
&lt;li&gt;You need to pay out in USD to recipients outside Africa via SWIFT without a separate integration&lt;/li&gt;
&lt;li&gt;You need a detailed transaction status model that maps to how African payment networks actually behave&lt;/li&gt;
&lt;li&gt;You want to store recipient payment method details once and reuse them across multiple payouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cleanest setup for a product that does both collection and disbursement is: Paystack or Flutterwave for inbound (they are better positioned for that job), and Afriex for outbound. They are complementary, not competing. Paystack collects your revenue, Afriex pays your contractors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started with Afriex disbursements
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://business.afriex.com/" rel="noopener noreferrer"&gt;Afriex Business API&lt;/a&gt; has a free sandbox environment. No approval gate, no manual credentials process to get through before you can test. Create an account, grab your sandbox API key from &lt;strong&gt;Settings &amp;gt; API Keys&lt;/strong&gt;, and you can run the full disbursement flow — customer creation, payment method attachment, transaction execution, webhook delivery — before you touch production.&lt;/p&gt;

&lt;p&gt;The integration guide covering the full three-step flow with code examples is in the &lt;a href="https://dev.to/afriex/build-a-freelancer-payout-platform-with-the-afriex-sdk-and-nextjs"&gt;freelancer payout platform tutorial&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>crossborder</category>
      <category>fintech</category>
      <category>stripe</category>
    </item>
    <item>
      <title>Afriex Webhook Integration Guide: Signature Verification, Event Handling, and Production Best Practices</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Mon, 25 May 2026 01:17:44 +0000</pubDate>
      <link>https://dev.to/afriex/afriex-webhook-integration-guide-signature-verification-event-handling-and-production-best-3c9o</link>
      <guid>https://dev.to/afriex/afriex-webhook-integration-guide-signature-verification-event-handling-and-production-best-3c9o</guid>
      <description>&lt;p&gt;When you create a transaction through the Afriex Business API, the response you get back is just the start. The transaction comes back with a status of &lt;code&gt;PENDING&lt;/code&gt;. What happens after that — whether it moves to &lt;code&gt;PROCESSING&lt;/code&gt;, &lt;code&gt;COMPLETED&lt;/code&gt;, &lt;code&gt;IN_REVIEW&lt;/code&gt;, or &lt;code&gt;FAILED&lt;/code&gt; arrives through webhooks.&lt;/p&gt;

&lt;p&gt;Most integration bugs in payment systems trace back to webhook handling, not the API calls themselves. Missed signature verification. Handlers that time out. Status updates applied twice. Fields read from parsed JSON instead of the raw body. These are the mistakes that cause payouts to look settled when they are not, or trigger duplicate notifications to your users.&lt;/p&gt;

&lt;p&gt;This article covers how Afriex webhooks work, every event the system fires, how to verify signatures correctly, how to build a handler that holds up in production, and how to test locally before you go live.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Afriex sends and when
&lt;/h2&gt;

&lt;p&gt;Afriex fires a signed HTTP POST to your configured webhook URL whenever a resource changes. Three resource types generate events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction events&lt;/strong&gt; are the ones you will interact with most. Every time a transaction is created or its status changes, Afriex fires either &lt;code&gt;TRANSACTION.CREATED&lt;/code&gt; or &lt;code&gt;TRANSACTION.UPDATED&lt;/code&gt;. Here's the full status vocabulary that a transaction moves through:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PENDING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transaction received, waiting to be processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROCESSING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Actively being processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COMPLETED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Settled successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SUCCESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alias for a settled transaction&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;Failed. Check &lt;code&gt;meta&lt;/code&gt; for details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CANCELLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancelled before processing started&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REFUNDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Funds returned to sender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN_REVIEW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Under manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REJECTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejected after review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RETRY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Being automatically retried by the network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNKNOWN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Status could not be determined. Contact support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Customer events&lt;/strong&gt; fire when a customer is created (&lt;code&gt;CUSTOMER.CREATED&lt;/code&gt;), their details are updated (&lt;code&gt;CUSTOMER.UPDATED&lt;/code&gt;), or they are deleted (&lt;code&gt;CUSTOMER.DELETED&lt;/code&gt;). These are useful for keeping your local customer records in sync with Afriex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payment method events&lt;/strong&gt; fire on creation (&lt;code&gt;PAYMENT_METHOD.CREATED&lt;/code&gt;), update (&lt;code&gt;PAYMENT_METHOD.UPDATED&lt;/code&gt;), and deletion (&lt;code&gt;PAYMENT_METHOD.DELETED&lt;/code&gt;). If a payment method is deleted on the Afriex side, your application needs to know so it can prompt the user to attach a new one before the next payout.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;CHECKOUT_SESSION.CREATED&lt;/code&gt; for checkout flows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you go live: allowlist the IP addresses
&lt;/h2&gt;

&lt;p&gt;This step catches developers off guard. Before Afriex can deliver webhooks to your server, your firewall must allow inbound traffic from Afriex's IP addresses. Webhook requests from any other IP should be blocked regardless of whether the signature is valid.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;IP Address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox&lt;/td&gt;
&lt;td&gt;&lt;code&gt;34.234.189.210&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;&lt;code&gt;34.197.33.100&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add these to your firewall or security group allowlist. Without this, Afriex webhook requests will be silently blocked before they reach your handler.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up your endpoint
&lt;/h2&gt;

&lt;p&gt;In your &lt;a href="https://business.afriex.com/" rel="noopener noreferrer"&gt;Afriex dashboard&lt;/a&gt;, go to &lt;strong&gt;Developers&lt;/strong&gt; then the &lt;strong&gt;Webhooks&lt;/strong&gt; tab. Paste your endpoint URL and save. Your webhook public key is on the same screen — copy it and store it as an environment variable. You will need it for signature verification.&lt;/p&gt;

&lt;p&gt;Staging and production use different public keys. Make sure you are using the correct one for each environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Signature verification
&lt;/h2&gt;

&lt;p&gt;Every webhook Afriex sends includes an &lt;code&gt;x-webhook-signature&lt;/code&gt; header. This is a Base64-encoded RSA-SHA256 signature of the raw request body, signed with Afriex's private key. You verify it using the public key from your dashboard.&lt;/p&gt;

&lt;p&gt;Two things to get right here that developers frequently get wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify against the raw body, not parsed JSON.&lt;/strong&gt; If you parse the body to JSON first and then try to verify the signature against the re-serialized string, it will fail. The signature was computed against the exact bytes Afriex sent. Any transformation — even a whitespace difference — breaks it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reject the request immediately if verification fails.&lt;/strong&gt; Do not process the payload. Do not log it as a real event. Return &lt;code&gt;400&lt;/code&gt; or &lt;code&gt;401&lt;/code&gt; and stop.&lt;/p&gt;

&lt;p&gt;Here is the correct verification implementation:&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;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;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;signature&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;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nx"&gt;publicKey&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;boolean&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;verifier&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;createVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RSA-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;verifier&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;rawBody&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;verifier&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;publicKey&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a Next.js API route, you need to read the raw body before any parsing happens:&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="c1"&gt;// src/app/api/webhooks/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&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;NextRequest&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;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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-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;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="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;NextResponse&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="s2"&gt;Missing signature&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;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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Read raw body before any parsing&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;isValid&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;signature&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;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;AFRIEX_WEBHOOK_PUBLIC_KEY&lt;/span&gt;&lt;span class="o"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&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;NextResponse&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="s2"&gt;Invalid signature&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;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="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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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="c1"&gt;// handle payload&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a Fastify application, you need to preserve the raw body before Fastify's JSON parser consumes it:&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="c1"&gt;// Register this plugin before routes&lt;/span&gt;
&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addContentTypeParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;parseAs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&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;parsed&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Attach raw string to request for webhook verification&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;h2&gt;
  
  
  The payload structure
&lt;/h2&gt;

&lt;p&gt;Every Afriex webhook follows the same envelope:&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;"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;"TRANSACTION.UPDATED"&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="err"&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;The &lt;code&gt;event&lt;/code&gt; field tells you what happened. The &lt;code&gt;data&lt;/code&gt; field contains the resource. Here is what each resource type looks like:&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction payload
&lt;/h3&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;"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;"TRANSACTION.UPDATED"&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;"transactionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"69d60071ab82306f11b03393"&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;"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;"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;"WITHDRAW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sourceAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.28847"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sourceCurrency"&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;"destinationAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"destinationCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NGN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"destinationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"690df3281c11eea59108fcaf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"69528240ba52c13b669fb239"&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;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ref-withdraw-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;"idempotencyKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"idem-withdraw-001"&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;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-08T07:14:57.444Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-08T07:15:30.000Z"&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;h3&gt;
  
  
  Customer payload
&lt;/h3&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;"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;"CUSTOMER.UPDATED"&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;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"698b0440cba7ec3daee9163d"&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;"John Smith"&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;"johnsmith@gmail.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;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+2348012345678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&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;"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;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-10T10:11:12.415Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-11T15:30:45.123Z"&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;h3&gt;
  
  
  Payment method payload
&lt;/h3&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;"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;"PAYMENT_METHOD.DELETED"&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;"paymentMethodId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"69f87b0dcc0ee96511560796"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BANK_ACCOUNT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6922e4520a53e858ab42efa8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"institution"&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;"institutionCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"058"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"institutionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GTBank"&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;"accountName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accountNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234567890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&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;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;h2&gt;
  
  
  Building a production-grade handler
&lt;/h2&gt;

&lt;p&gt;A webhook handler has one job: acknowledge receipt fast, then process asynchronously. Afriex expects a &lt;code&gt;2xx&lt;/code&gt; response within about 5 seconds. If your handler does database writes, sends emails, or calls other APIs synchronously before returning, you will hit that window under any real load.&lt;/p&gt;

&lt;p&gt;The pattern that holds up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify signature&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;200&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;Push the raw payload to a queue&lt;/li&gt;
&lt;li&gt;Process in a background worker
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lean handler — verify, acknowledge, enqueue&lt;/span&gt;
&lt;span class="k"&gt;export&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;POST&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;NextRequest&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;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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-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;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="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;NextResponse&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="s2"&gt;Missing signature&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;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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;isValid&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;signature&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;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;AFRIEX_WEBHOOK_PUBLIC_KEY&lt;/span&gt;&lt;span class="o"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&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;NextResponse&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="s2"&gt;Invalid signature&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;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="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Enqueue for async processing — do not process inline&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="na"&gt;payload&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;parse&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="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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;received&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Handle each event correctly
&lt;/h3&gt;

&lt;p&gt;Not every status requires the same response. Here is a decision map for transaction events:&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;handleTransactionEvent&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;TransactionWebhookPayload&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;transactionId&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="o"&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="c1"&gt;// Update your database first&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateTransactionStatus&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="nx"&gt;status&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;status&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="s2"&gt;PROCESSING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Informational — no user-facing action needed&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="s2"&gt;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;case&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="c1"&gt;// Terminal success — notify user, update UI state&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendPayoutConfirmationEmail&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;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="s2"&gt;IN_REVIEW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Compliance hold — notify user that payout is under review&lt;/span&gt;
      &lt;span class="c1"&gt;// Do not mark as failed. Wait for further updates.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifyPayoutUnderReview&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;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="s2"&gt;RETRY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Network is retrying automatically — no action needed&lt;/span&gt;
      &lt;span class="c1"&gt;// Do not alarm the user&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="s2"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;REJECTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Terminal failure — notify user, allow them to retry&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendPayoutFailedAlert&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="nx"&gt;status&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="s2"&gt;UNKNOWN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Indeterminate — log and alert your team to investigate&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alertTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Transaction &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; reached UNKNOWN status`&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="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 &lt;code&gt;IN_REVIEW&lt;/code&gt; and &lt;code&gt;RETRY&lt;/code&gt; statuses are the ones most developers handle incorrectly. &lt;code&gt;IN_REVIEW&lt;/code&gt; is not a failure — it is a compliance hold that will resolve into &lt;code&gt;COMPLETED&lt;/code&gt; or &lt;code&gt;REJECTED&lt;/code&gt;. If you mark it as failed and notify the user, you will have unhappy users chasing payouts that are actually in progress. &lt;code&gt;RETRY&lt;/code&gt; means the network is handling it automatically. No action needed on your end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make your handler idempotent
&lt;/h3&gt;

&lt;p&gt;Afriex retries webhook delivery up to 12 times with exponential backoff. Your handler will receive the same event more than once. That is by design. Your code needs to handle it gracefully.&lt;/p&gt;

&lt;p&gt;The simplest approach: store a record of processed webhook event IDs and skip any you have already handled.&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;processWebhookEvent&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;WebhookPayload&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;eventId&lt;/span&gt; &lt;span class="o"&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;payload&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="s2"&gt;:&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;transactionId&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;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;updatedAt&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;// Check if we have already processed this exact event&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="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&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;processedWebhooks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processedWebhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&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="nx"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Already handled — acknowledge and return&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;// Process the event&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleTransactionEvent&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="c1"&gt;// Mark as processed&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;insert&lt;/span&gt;&lt;span class="p"&gt;(&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;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;processedAt&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the idempotency key you can use a combination of &lt;code&gt;event&lt;/code&gt; type, resource ID, and &lt;code&gt;updatedAt&lt;/code&gt; timestamp. This way, the same status update arriving twice is treated as a duplicate and skipped, but a genuine status change on the same transaction (e.g., &lt;code&gt;PROCESSING&lt;/code&gt; followed by &lt;code&gt;COMPLETED&lt;/code&gt;) is treated as two distinct events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle customer and payment method events
&lt;/h3&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;handleCustomerEvent&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;CustomerWebhookPayload&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;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;payload&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="s2"&gt;CUSTOMER.UPDATED&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;// Keep your local record in sync&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateLocalCustomer&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;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&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;name&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;data&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="p"&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;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CUSTOMER.DELETED&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;// Mark the customer as removed in your DB&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markCustomerDeleted&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;customerId&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;handlePaymentMethodEvent&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;PaymentMethodWebhookPayload&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;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;payload&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="s2"&gt;PAYMENT_METHOD.DELETED&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;// Remove from your DB and flag the customer as needing a new payout method&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;removePaymentMethod&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;paymentMethodId&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;flagCustomerNeedsPaymentMethod&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;customerId&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;h2&gt;
  
  
  Retry behavior
&lt;/h2&gt;

&lt;p&gt;Afriex retries failed webhook deliveries up to 12 times. The schedule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30s → 1m → 2m → 4m → 8m → 16m → 32m → 1h → 2h → 4h → 8h → 16h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A delivery is considered failed if your endpoint returns a non-2xx status or does not respond within about 5 seconds. This means your handler timing out is treated the same as a hard error — Afriex will retry.&lt;/p&gt;

&lt;p&gt;Two practical implications. First, your handler must respond quickly (within 5 seconds) regardless of what processing needs to happen — hence the enqueue-and-return pattern above. Second, you should never rely solely on webhooks for reconciliation. Build a polling fallback: periodically call &lt;code&gt;GET /api/v1/transaction/:id&lt;/code&gt; for transactions that have been in a non-terminal status for longer than expected. Webhooks are the fast path. The API is the source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing locally
&lt;/h2&gt;

&lt;p&gt;Afriex provides a sandbox-only endpoint for firing real signed webhooks against your local handler without needing to manufacture underlying activity. You create an entity (a customer, payment method, or transaction) in sandbox, then call the trigger endpoint with the entity ID and the event name you want to test.&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://sandbox.api.afriex.com/api/v1/webhooks/trigger &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;--header&lt;/span&gt; &lt;span class="s1"&gt;'x-api-key: your-sandbox-api-key'&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;'{
    "event": "TRANSACTION.UPDATED",
    "entityId": "69d60071ab82306f11b03393"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Afriex will send a real signed webhook to your configured callback URL using that entity as the payload. Because it is a real signed payload, your signature verification code runs exactly as it would in production.&lt;/p&gt;

&lt;p&gt;To receive it locally, expose your dev server with &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Register the HTTPS URL ngrok gives you as your webhook URL in the Afriex sandbox dashboard, then fire the trigger. You can test every event type this way — &lt;code&gt;CUSTOMER.CREATED&lt;/code&gt;, &lt;code&gt;PAYMENT_METHOD.DELETED&lt;/code&gt;, &lt;code&gt;TRANSACTION.UPDATED&lt;/code&gt; with any status — against a real entity in sandbox.&lt;/p&gt;

&lt;p&gt;The trigger endpoint returns &lt;code&gt;403 Forbidden&lt;/code&gt; in production, so there is no risk of accidentally firing test webhooks against your live environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist before going live
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Afriex IP addresses added to your server allowlist (&lt;code&gt;34.197.33.100&lt;/code&gt; for production)&lt;/li&gt;
&lt;li&gt;[ ] Webhook public key stored as an environment variable, not hardcoded&lt;/li&gt;
&lt;li&gt;[ ] Signature verification runs against raw body bytes, not parsed JSON&lt;/li&gt;
&lt;li&gt;[ ] Handler returns &lt;code&gt;2xx&lt;/code&gt; within 5 seconds&lt;/li&gt;
&lt;li&gt;[ ] Processing happens asynchronously after acknowledgement&lt;/li&gt;
&lt;li&gt;[ ] All 11 transaction statuses handled explicitly — including &lt;code&gt;IN_REVIEW&lt;/code&gt;, &lt;code&gt;RETRY&lt;/code&gt;, and &lt;code&gt;UNKNOWN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Handler is idempotent — safe to receive the same event multiple times&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;PAYMENT_METHOD.DELETED&lt;/code&gt; event triggers a flag in your database&lt;/li&gt;
&lt;li&gt;[ ] Polling fallback implemented for transactions stuck in non-terminal status&lt;/li&gt;
&lt;li&gt;[ ] Tested all event types using the sandbox trigger endpoint&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full webhook reference is at &lt;a href="https://docs.afriex.com/api-reference/endpoint/webhooks/introduction" rel="noopener noreferrer"&gt;docs.afriex.com/api-reference/endpoint/webhooks/introduction&lt;/a&gt;. The transaction API reference, including the full request and response schema, is at &lt;a href="https://docs.afriex.com/api-reference/endpoint/transactions/create" rel="noopener noreferrer"&gt;docs.afriex.com/api-reference/endpoint/transactions/create&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>afriex</category>
      <category>devex</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How Agentic AI Is Changing Cross-Border Payments (and What It Means for Developers)</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Sun, 24 May 2026 06:21:36 +0000</pubDate>
      <link>https://dev.to/afriex/how-agentic-ai-is-changing-cross-border-payments-and-what-it-means-for-developers-3m12</link>
      <guid>https://dev.to/afriex/how-agentic-ai-is-changing-cross-border-payments-and-what-it-means-for-developers-3m12</guid>
      <description>&lt;p&gt;For most of the last decade, AI in payments meant one thing: fraud detection. A model sitting downstream, flagging suspicious transactions after the fact. Useful, but passive. The system still required a human or deterministic code to decide what to do next.&lt;/p&gt;

&lt;p&gt;That is changing fast. &lt;a href="https://thepaypers.com/payments/expert-views/2025-payments-retrospective-the-reshaping-forces-of-agentic-ai-a2a-rtps-and-cbdcs" rel="noopener noreferrer"&gt;Agentic AI emerged as the breakout technology of 2025, moving from demos into regulated payment workflows&lt;/a&gt;. The difference is not the model. It is the architecture. Agentic systems do not just classify or predict. They plan, execute multi-step workflows, and take action across external systems without a human in the loop for every step. In payments, that shift has real consequences for how infrastructure gets built and what developers need to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  What agentic AI actually means in a payments context
&lt;/h2&gt;

&lt;p&gt;The term gets used loosely, so a working definition is worth establishing. An AI agent in a payment context is a system that receives a high-level goal, decides what sequence of API calls to make to achieve it, executes them, handles failures, and reports the outcome, with no human approving each individual step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.elibrary.imf.org/view/journals/068/2026/004/article-A001-en.xml" rel="noopener noreferrer"&gt;The IMF's May 2026 note on agentic AI in payments&lt;/a&gt; describes the scope of experimentation as expanding rapidly: from fraud detection and compliance monitoring to treasury optimization and cross-border payment orchestration. &lt;a href="https://www.fenwick.com/insights/publications/is-2026-the-year-of-agentic-payments" rel="noopener noreferrer"&gt;Fenwick's 2026 agentic payments analysis&lt;/a&gt; draws the line clearly: unlike traditional autopay automation, agentic AI makes decisions and takes actions to achieve goals. It is not executing a predefined script.&lt;/p&gt;

&lt;p&gt;The practical difference for a developer is this: instead of writing code that calls &lt;code&gt;get_balance&lt;/code&gt;, evaluates the result, and conditionally calls &lt;code&gt;create_transaction&lt;/code&gt;, you expose those capabilities as tools and let the model decide the sequence based on a stated goal. The orchestration logic moves from your codebase to the model. The code you write shrinks. The capability surface expands.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where agentic payment automation is already happening
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.fxcintel.com/research/analysis/ai-earnings-mentions-2025" rel="noopener noreferrer"&gt;A March 2026 analysis of earnings calls across 24 companies in the cross-border payments space&lt;/a&gt; found AI mentions surging significantly year over year. The themes were not hypothetical. NatWest said AI agents can "execute complex banking workflows" on behalf of customers. Remitly announced plans to deploy agentic technology across productivity, fraud reduction, and decision-making in 2026.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://techinformed.com/agentic-ai-and-more-to-reshape-fintech-in-2026/" rel="noopener noreferrer"&gt;Agentic AI is moving toward anticipating intent, verifying identity, detecting fraud, and authorizing transactions in real time across platforms&lt;/a&gt;, all in a single autonomous workflow rather than across separate systems. Compliance teams are using agentic AI to shift from static rule-based watchlist screening to continuous, trigger-based monitoring. &lt;a href="https://substack.com/@samboboev/note/c-202832396" rel="noopener noreferrer"&gt;Watchlist screening currently generates 90 to 95 percent false positives&lt;/a&gt;, and agentic systems with richer contextual reasoning are actively pushing that number down.&lt;/p&gt;

&lt;p&gt;For cross-border payment infrastructure specifically, the primary application is treasury optimization and payment orchestration. &lt;a href="https://substack.com/@samboboev/note/c-202832396" rel="noopener noreferrer"&gt;Cross-border flows hit $190 trillion annually&lt;/a&gt; and legacy systems still route most of that through multi-step correspondent banking chains. Agentic payment systems can evaluate multiple rail options in real time, select the optimal one for a given corridor and amount, monitor for settlement status, and escalate to a human only when a transaction falls outside expected parameters.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architectural challenge this creates
&lt;/h2&gt;

&lt;p&gt;The IMF note identifies what it calls a central architectural challenge: as AI agents gain the ability to initiate and execute payment transactions autonomously, the traditional assumption that a human authorizes each individual transaction breaks down. The legal and liability frameworks built around that assumption do not map cleanly onto autonomous agent behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.fenwick.com/insights/publications/is-2026-the-year-of-agentic-payments" rel="noopener noreferrer"&gt;Existing financial and consumer protection laws built around human-decisioned transactions may not appropriately address the challenges raised by agentic payments&lt;/a&gt;. Companies building agentic payment automation need to navigate unsettled questions under AI laws, money transmitter regimes, and authorization frameworks simultaneously.&lt;/p&gt;

&lt;p&gt;For developers, this creates two concrete requirements. First, every action an agent takes on behalf of a user must be logged with enough detail to reconstruct exactly what the agent decided, what information it had at the time, and what it executed. Immutable audit logs are not a nice-to-have in agentic payment systems. They are the primary mechanism for accountability. Second, agent scope must be explicitly bounded. An agent authorized to send payroll disbursements should not be able to initiate arbitrary transactions outside that context. Tool-level permission scoping, not just API key permissions, is the right model here.&lt;/p&gt;




&lt;h2&gt;
  
  
  What agentic orchestration requires from payment APIs
&lt;/h2&gt;

&lt;p&gt;The shift toward autonomous payment systems changes the requirements for the payment APIs agents build on. These requirements are stricter than what human-driven integrations demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool descriptions carry the same weight as endpoint documentation.&lt;/strong&gt; When a human developer integrates an API, they read the docs and write code. When an AI agent integrates a payment API, it reads the tool descriptions and decides what to call. An ambiguous tool description produces wrong agent behavior the same way an ambiguous endpoint contract produces bugs. Payment infrastructure providers who want their APIs used in agentic payment workflows need to treat tool descriptions as a first-class product concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error responses need to be machine-interpretable, not just human-readable.&lt;/strong&gt; An agent that receives "Something went wrong" cannot decide whether to retry, escalate, or abort. Structured error codes like &lt;code&gt;INSUFFICIENT_BALANCE&lt;/code&gt;, &lt;code&gt;FX_RATE_EXPIRED&lt;/code&gt;, and &lt;code&gt;PAYMENT_METHOD_INVALID&lt;/code&gt; give the model the information it needs to make the right decision without human intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction status granularity matters more than it did before.&lt;/strong&gt; In a human-driven integration, a developer can decide how to handle an ambiguous status. In an agentic payment workflow, the agent needs enough signal to act correctly on its own. A payment API that returns &lt;code&gt;PENDING&lt;/code&gt; for multiple distinct states forces the agent to guess. A well-designed one surfaces the full status vocabulary the underlying network produces: &lt;code&gt;PENDING&lt;/code&gt;, &lt;code&gt;PROCESSING&lt;/code&gt;, &lt;code&gt;IN_REVIEW&lt;/code&gt;, &lt;code&gt;COMPLETED&lt;/code&gt;, &lt;code&gt;FAILED&lt;/code&gt;, &lt;code&gt;REJECTED&lt;/code&gt;, &lt;code&gt;RETRY&lt;/code&gt;, &lt;code&gt;REFUNDED&lt;/code&gt;. Each status maps to a different agent decision. &lt;code&gt;IN_REVIEW&lt;/code&gt; means wait and poll. &lt;code&gt;RETRY&lt;/code&gt; means the network is handling it. &lt;code&gt;REJECTED&lt;/code&gt; means surface to a human. The Afriex Business API exposes exactly this vocabulary on every transaction, which is one reason it is well-suited as infrastructure for agentic payment automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency is non-negotiable.&lt;/strong&gt; Agents retry. Networks fail. &lt;a href="https://medium.com/@jyc.dev/idempotency-in-software-engineering-why-it-matters-and-how-to-implement-it-2025-guide-c1ef8ad21965" rel="noopener noreferrer"&gt;Without idempotency keys, payment APIs risk duplicate transactions that are costly and difficult to reverse&lt;/a&gt;. An autonomous payment system without idempotent operations is a double-payment incident waiting to happen. The Afriex SDK accepts an idempotency key on every transaction creation call, which means retrying a failed disbursement job is safe by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  The African corridor context
&lt;/h2&gt;

&lt;p&gt;For developers building agentic payment automation for African markets, the infrastructure complexity is higher than most global payment APIs assume, and the choice of payment API matters more as a result.&lt;/p&gt;

&lt;p&gt;Mobile money is the dominant payment rail in East and West Africa, not cards and not bank transfers. FX volatility in corridors like NGN/USD means a rate that is valid at the moment an agent job is created may be meaningfully different at the moment of settlement. Transaction statuses like &lt;code&gt;IN_REVIEW&lt;/code&gt; reflect real compliance holds that African payment networks produce, not just generic processing delays. An agent operating in this environment needs access to tools that reflect those realities rather than abstracting them away.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://business.afriex.com/" rel="noopener noreferrer"&gt;Afriex Business API&lt;/a&gt; is built against this infrastructure directly. Mobile money, bank transfers, SWIFT, and local payment channels are all first-class integrations. Exchange rates are live across NGN, KES, GHS, GBP, and other African corridor pairs. The &lt;a href="https://docs.afriex.com/mcp/introduction" rel="noopener noreferrer"&gt;Afriex MCP server&lt;/a&gt; exposes the full API surface as 22 callable tools for agentic workflows: &lt;code&gt;get_rates&lt;/code&gt; for live rates before committing to a disbursement, &lt;code&gt;get_balance&lt;/code&gt; to verify funds before a payroll run starts, &lt;code&gt;resolve_payment_method&lt;/code&gt; to verify a recipient account before attaching it, &lt;code&gt;create_transaction&lt;/code&gt; with idempotency key support, and &lt;code&gt;get_transaction&lt;/code&gt; for status polling after execution. The full transaction status vocabulary, including &lt;code&gt;IN_REVIEW&lt;/code&gt;, &lt;code&gt;RETRY&lt;/code&gt;, and &lt;code&gt;REJECTED&lt;/code&gt;, is surfaced through webhooks so an agent always has enough signal to decide its next action correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to build now versus what to watch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Build now: agentic payroll disbursement.&lt;/strong&gt; The use case is clear, the failure modes are well-understood, and the liability surface is bounded because a human approves the payroll run before the agent executes it. The agent's autonomy is scoped to execution, not authorization. This is the lowest-risk entry point for agentic payment automation and has the most direct ROI. The architecture for this, including how it integrates with the Afriex Business API, is covered in the &lt;a href="https://dev.to/afriex"&gt;companion architecture document&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build now: agentic FX monitoring and settlement timing.&lt;/strong&gt; An agent that watches a currency corridor, evaluates whether the current rate is within a threshold, and triggers settlement when conditions are met is straightforward to implement using the Afriex MCP server's &lt;code&gt;get_rates&lt;/code&gt; tool on a schedule. It is immediately valuable for any business running frequent cross-border payment flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch: fully autonomous payment authorization.&lt;/strong&gt; Agents that can initiate arbitrary payments based on their own assessment of conditions, without a human approving each batch, sit in genuinely unsettled legal territory. &lt;a href="https://www.fenwick.com/insights/publications/is-2026-the-year-of-agentic-payments" rel="noopener noreferrer"&gt;The legal framework for autonomous payment authorization is unresolved&lt;/a&gt;, and building ahead of regulatory clarity is a risk most developers should not take on without specific legal guidance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch: multi-agent payment orchestration.&lt;/strong&gt; Chains of agents handing off payment decisions to each other across organizational boundaries are one of the most technically and legally complex areas in agentic AI right now. &lt;a href="https://www.elibrary.imf.org/view/journals/068/2026/004/article-A001-en.xml" rel="noopener noreferrer"&gt;The IMF note&lt;/a&gt; identifies this as a central challenge. The infrastructure protocols for secure multi-agent communication across payment networks are still being standardized.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Agentic AI does not change what cross-border payment infrastructure needs to do. It changes who is doing the orchestrating. The requirements for the underlying payment API layer get stricter as a result: machine-interpretable error codes, granular transaction status vocabularies, idempotent operations, and tool descriptions precise enough for a model to act on correctly without human clarification.&lt;/p&gt;

&lt;p&gt;For developers, the opportunity in agentic payment automation is real and immediate in bounded use cases. The risk is real too, and proportional to how much autonomous authorization you give the agent. Start with execution autonomy, keep authorization human, and build the audit trail as if regulators will read it. Because eventually, they will.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>afriex</category>
      <category>agents</category>
    </item>
    <item>
      <title>Build Cross-Border Payment AI Agents with the Afriex MCP Server</title>
      <dc:creator>Victory Lucky</dc:creator>
      <pubDate>Sat, 02 May 2026 04:20:50 +0000</pubDate>
      <link>https://dev.to/afriex/build-cross-border-payment-ai-agents-with-the-afriex-mcp-server-2l5j</link>
      <guid>https://dev.to/afriex/build-cross-border-payment-ai-agents-with-the-afriex-mcp-server-2l5j</guid>
      <description>&lt;p&gt;Payment integrations are straightforward until they are not. A single payout involves checking a balance, resolving a bank code, verifying an account number, looking up a live exchange rate, and then executing the transaction — each step a separate API call, each one requiring code you write and maintain.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.afriex.com/mcp/introduction" rel="noopener noreferrer"&gt;Afriex MCP server&lt;/a&gt; lets an AI agent handle that entire sequence through natural language. It exposes the complete &lt;a href="https://docs.afriex.com/" rel="noopener noreferrer"&gt;Afriex Business API&lt;/a&gt; as 20+ callable tools — customers, transactions, payment methods, exchange rates, and balances — that any MCP-compatible client can discover and use without SDK wiring.&lt;/p&gt;

&lt;p&gt;Connect it to Claude Desktop, Claude Code, or Cursor and your AI assistant can register customers, attach bank accounts and mobile money wallets, fetch live NGN/KES/GHS rates, and trigger cross-border payouts — all from a single prompt.&lt;/p&gt;

&lt;p&gt;If you are not yet familiar with Model Context Protocol and how it works, &lt;a href="https://dev.to/codewithveek/model-context-protocol-the-standard-that-lets-ai-agents-actually-do-things-4coj"&gt;this article covers the protocol, architecture, and security model&lt;/a&gt; before you continue here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20+ tools
&lt;/h2&gt;

&lt;p&gt;The server endpoint is &lt;code&gt;https://mcp.afriex.com/mcp&lt;/code&gt;. Full reference at &lt;a href="https://docs.afriex.com/mcp/introduction" rel="noopener noreferrer"&gt;docs.afriex.com/mcp/introduction&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authenticate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets your API key and environment for the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session_info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns current session config — masked API key, environment, base URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Customers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a customer with name, email, phone, and country code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_customers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns a paginated list of customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches a customer by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete_customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes a customer by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update_customer_kyc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates KYC information for a customer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Transactions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_transaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a &lt;code&gt;DEPOSIT&lt;/code&gt;, &lt;code&gt;WITHDRAW&lt;/code&gt;, or &lt;code&gt;SWAP&lt;/code&gt; transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_transactions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns a paginated list of transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_transaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches a transaction by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Payment methods
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_payment_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a payment method: &lt;code&gt;BANK_ACCOUNT&lt;/code&gt;, &lt;code&gt;SWIFT&lt;/code&gt;, &lt;code&gt;MOBILE_MONEY&lt;/code&gt;, &lt;code&gt;UPI&lt;/code&gt;, &lt;code&gt;INTERAC&lt;/code&gt;, &lt;code&gt;WE_CHAT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_payment_methods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns a paginated list of payment methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_payment_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches a payment method by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete_payment_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes a payment method by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_institutions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists banks and mobile money providers by country and channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resolve_institution_codes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolves a SWIFT code or routing number to a bank name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resolve_payment_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolves recipient info by account number or phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_crypto_wallet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets or creates a USDT/USDC crypto wallet (production only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_virtual_account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets or creates a virtual account (production only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Organization
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_balance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches wallet balances for specified currencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_rates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets live exchange rates across supported corridors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topup_balance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tops up sandbox balance (development only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Connecting your client
&lt;/h2&gt;

&lt;p&gt;You will need an Afriex Business API key from the &lt;a href="https://business.afriex.com/" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;. Pass it via HTTP headers — this keeps credentials out of the conversation context and authenticates every tool call automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"afriex"&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;"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://mcp.afriex.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"x-afriex-api-key"&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-api-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"x-afriex-environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"development"&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;Restart Claude Desktop. The Afriex tools will appear in the tools panel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;.claude/settings.json&lt;/code&gt; in your project:&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;"mcpServers"&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;"afriex"&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;"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://mcp.afriex.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"x-afriex-api-key"&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-api-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"x-afriex-environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"development"&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;Or via the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add afriex &lt;span class="nt"&gt;--transport&lt;/span&gt; http https://mcp.afriex.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project root:&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;"mcpServers"&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;"afriex"&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;"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://mcp.afriex.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"x-afriex-api-key"&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-api-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"x-afriex-environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"development"&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;Or go to &lt;strong&gt;Cursor Settings → MCP → Add new MCP Server&lt;/strong&gt;, set type to &lt;strong&gt;URL&lt;/strong&gt;, and enter &lt;code&gt;https://mcp.afriex.com/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Without HTTP headers
&lt;/h3&gt;

&lt;p&gt;If your client does not support custom headers, connect to the server URL without them and use the &lt;code&gt;authenticate&lt;/code&gt; tool as your first prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Authenticate with API key &lt;code&gt;your-api-key&lt;/code&gt; in the production environment."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The server stores your credentials for the session and uses them for all subsequent tool calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cross-border payout agent
&lt;/h3&gt;

&lt;p&gt;A complete payout as an MCP tool chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;list_institutions&lt;/code&gt; — resolve the correct bank or mobile money code for the recipient's country&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_customer&lt;/code&gt; — register the recipient&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_customer_kyc&lt;/code&gt; — complete KYC&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolve_payment_method&lt;/code&gt; — verify the account number before attaching it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_payment_method&lt;/code&gt; — attach the verified bank account or mobile wallet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_rates&lt;/code&gt; — preview the live exchange rate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_balance&lt;/code&gt; — confirm sufficient funds before committing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_transaction&lt;/code&gt; with &lt;code&gt;type: "WITHDRAW"&lt;/code&gt; — execute the payout&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_transaction&lt;/code&gt; — poll settlement status&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent chains these in sequence and branches on results — it will not call &lt;code&gt;create_transaction&lt;/code&gt; if &lt;code&gt;get_balance&lt;/code&gt; returns insufficient funds. You state the intent, the model decides the sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payroll disbursement agent
&lt;/h3&gt;

&lt;p&gt;An agent that reads a payroll file and disburses to each employee across multiple African corridors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;get_rates&lt;/code&gt; per currency corridor to confirm converted amounts upfront&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_balance&lt;/code&gt; to verify total funds before any disbursement starts&lt;/li&gt;
&lt;li&gt;For each employee: &lt;code&gt;list_customers&lt;/code&gt; to check if they exist, &lt;code&gt;create_customer&lt;/code&gt; if not, &lt;code&gt;create_payment_method&lt;/code&gt; if no payout method is on file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_transaction&lt;/code&gt; per employee&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_transaction&lt;/code&gt; to confirm each settlement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a customer already exists, the agent skips creation. If funds run short mid-run, it stops and reports before making the next call rather than failing silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational queries from your IDE
&lt;/h3&gt;

&lt;p&gt;With the server connected in Claude Code or Cursor, you can query your Afriex account without leaving your editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is the current NGN to USD rate?" → &lt;code&gt;get_rates&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"List my last 10 transactions and flag any that failed" → &lt;code&gt;list_transactions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"Check my USD and GBP balances" → &lt;code&gt;get_balance&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"What is GTBank Nigeria's institution code?" → &lt;code&gt;list_institutions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;"Top up my sandbox NGN balance by 500,000" → &lt;code&gt;topup_balance&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FX rate monitoring agent
&lt;/h3&gt;

&lt;p&gt;An agent that watches a currency pair and acts when a target rate is hit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;get_rates&lt;/code&gt; on a schedule for a specific corridor&lt;/li&gt;
&lt;li&gt;Compare against a configured threshold&lt;/li&gt;
&lt;li&gt;When the rate crosses, trigger &lt;code&gt;create_transaction&lt;/code&gt; or surface an alert&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful for businesses that time cross-border settlements against rate movements rather than executing at arbitrary times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sandbox and environment
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;x-afriex-environment&lt;/code&gt; to &lt;code&gt;development&lt;/code&gt; for sandbox testing. The &lt;code&gt;topup_balance&lt;/code&gt; tool funds your sandbox wallet with any currency and amount, so you can test the full tool chain, customer creation through transaction settlement, without touching live funds.&lt;/p&gt;

&lt;p&gt;Switch to &lt;code&gt;production&lt;/code&gt; in the header when ready to go live.&lt;/p&gt;




&lt;h2&gt;
  
  
  MCP versus the Afriex SDK
&lt;/h2&gt;

&lt;p&gt;They are not alternatives — they serve different layers.&lt;/p&gt;

&lt;p&gt;Use the &lt;strong&gt;SDK&lt;/strong&gt; for production applications: deterministic code paths, typed inputs and outputs, full error handling control, and integration with your own data layer. The &lt;a href="https://dev.to/afriex/build-a-freelancer-payout-platform-with-the-afriex-sdk-and-nextjs"&gt;freelancer payout platform tutorial&lt;/a&gt; is a complete walkthrough of that approach.&lt;/p&gt;

&lt;p&gt;Use the &lt;strong&gt;MCP server&lt;/strong&gt; when an AI model is doing the orchestrating: agentic workflows, internal natural language tools, IDE queries during development, and rapid prototyping before you formalize an integration.&lt;/p&gt;

&lt;p&gt;Most production setups use both, the SDK for core application logic, the MCP server for the agentic layer on top.&lt;/p&gt;

&lt;p&gt;Full tool reference and setup docs at &lt;a href="https://docs.afriex.com/mcp/introduction" rel="noopener noreferrer"&gt;docs.afriex.com/mcp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>afriex</category>
    </item>
  </channel>
</rss>
