Server to server integration
Full server to server integration is possible only after a merchant KYB process
It's possible to do a full on-ramp flow by using only merchant API. Here you can find how to send API requests correctly. The full workflow should look like this:
Get a list of supported countries and their payment channels.
Pick a country, payment channel, and blockchain asset. Get order limits using these values.
Get the user's KYC status and check if they need to pass a KYC process. If they need a KYC, submit the document information and check the KYC status until it's accepted.
Get the best offer , to get the quoteId, understand how much a user should pay, and what additional information is required from a user to create an order.
Create an order using the user's email, desired amount, blockchain asset, country, payment channel, additional data required from a user, and quoteId. Some orders may require verifying an OTP code sent to a user.
Getting countries, payment channels, blockchain assets and order limits
Let's get a list of supported countries , the response would be like this:
[
{
"name": "Nigeria",
"countryIsoCode": "NG",
"currencyIsoCode": "NGN",
"paymentChannels": [
{
"paymentChannel": "bank",
"description": "Bank transfer",
"requiresCarrier": false,
"carriers": []
},
{
"paymentChannel": "airtime",
"description": "Airtime",
"requiresCarrier": true,
"carriers": [
{
"id": "618e43914f57e07d255ff357",
"name": "Airtel Nigeria"
},
...
]
}
]
},
...
]
We see that Nigeria is supported for on-ramp and has payment channels: bank and airtime.
Let's pick this country and bank payment channel. Let's get a list of supported blockchain assets:
[
{
"network": "POLYGON",
"asset": "USDC",
"canOfframp": true,
"canOnramp": true
},
{
"network": "ETHEREUM",
"asset": "USDC",
"canOfframp": true,
"canOnramp": true
},
...
]
We see that POLYGON USDC is supported for on-ramp; let's pick it.
Now, let's check order limits for Nigeria, bank payment channel and POLYGON USDC asset. Request query params:
?countryIsoCode=NG&paymentChannel=bank&network=POLYGON&asset=USDC
Response:
{
"minUsd": 1,
"maxUsd": 100,
"minLocalCurrency": 1534,
"maxLocalCurrency": 153376,
"minCrypto": 1,
"maxCrypto": 100
}
So, now we understand that a user can buy from 1 to 100 POLYGON USDC and can pay from 1534 to 153376 NGN.
KYC
Let's check a user's KYC status to determine if we need to sumbit a KYC documents. Request body:
{
email: "[email protected]",
countryIsoCode: "NG"
}
Response:
{
"reachedKycLimit": false,
"basicDocuments": [
{
"_id": "67da909b739fc481aa525c45",
"type": "basic",
"title": "BVN",
"value": "BVN",
"requiredFields": {
"first_name": {
"type": "string",
"label": "First Name",
"required": true
},
"last_name": {
"type": "string",
"label": "Last Name",
"required": true
},
"dob": {
"type": "date",
"label": "Date of birth",
"required": true
},
"email": {
"type": "email",
"label": "Email",
"required": true
},
"id_number": {
"type": "string",
"label": "BVN Number",
"required": true,
"format": "00000000000",
"regexp": "^[0-9]{11}$"
}
}
},
...
],
"advancedDocuments": [
{
"_id": "67da93c0dfd3a00f3380b857",
"type": "advanced",
"title": "Driving License",
"value": "DRIVERS_LICENSE",
"requiredFields": {
"first_name": {
"type": "string",
"label": "First Name",
"required": true
},
"last_name": {
"type": "string",
"label": "Last Name",
"required": true
},
"dob": {
"type": "date",
"label": "Date of birth",
"required": true
},
"email": {
"type": "email",
"label": "Email",
"required": true
},
"images": {
"type": "smile-identity-images",
"label": "Verification images",
"required": true
}
}
},
...
],
"kycRules": {
"onramp": [
{ min: 0, max: 10, type: 'none' },
{ min: 10, max: 50, type: 'basic' },
{ min: 50, max: 100, type: 'advanced' },
],
"offramp": [
{ min: 0, max: 7, type: 'none' },
{ min: 7, max: 35, type: 'basic' },
{ min: 35, max: 100, type: 'advanced' },
],
},
}
We see that there's no passedKycType field which means that the user haven't completed a KYC process in our system. Moreover, we see a list of supported documents for basic and advanced KYC.
The kycRules field indicates that we don't need a KYC for orders below $10, need a basic KYC for the $10-50(not including) range, and need an advanced KYC for the $50-100 range.
For demonstration purposes, let's at first complete the basic KYC and then the advanced one. Let's pick a basic document to submit:
{
"_id": "67da909b739fc481aa525c45",
"type": "basic",
"title": "BVN",
"value": "BVN",
"requiredFields": {
"first_name": {
"type": "string",
"label": "First Name",
"required": true
},
"last_name": {
"type": "string",
"label": "Last Name",
"required": true
},
"dob": {
"type": "date",
"label": "Date of birth",
"required": true
},
"email": {
"type": "email",
"label": "Email",
"required": true
},
"id_number": {
"type": "string",
"label": "BVN Number",
"required": true,
"format": "00000000000",
"regexp": "^[0-9]{11}$"
}
}
}
We need to build an object with keys described under "requiredFields", like this:
{
"first_name": "Joe",
"last_name": "Doe",
"dob": "2000-01-01T00:00:00.000Z",
"email": "[email protected]",
"id_number": "00000000000"
}
Then we submit it using the user's email, countryIsoCode, document ID, and these fields.
Request body:
{
"email": "[email protected]",
"countryIsoCode": "NG",
"documentId": "67da909b739fc481aa525c45",
"userFields": {
"first_name": "Joe",
"last_name": "Doe",
"dob": "2000-01-01T00:00:00.000Z",
"email": "[email protected]",
"id_number": "00000000012",
},
}
Let's check a user's KYC status again:
{
"passedKycType": "basic",
"kycStatus": "approved",
"kycStatusDescription": "Partial Match",
...
}
We see that a user passed the KYC and now has passedKycType = basic. If KYC check was still pending the response would be like the following:
{
"kycStatus": "initiated",
}
Failed KYC would look like the following:
{
"kycStatus": "rejected",
"kycStatusDescription": "Unable to verify ID - Result Not Found",
}
In case of rejected KYC you can try to submit a new one untill reachedKycLimit = true, thereafter, you need to contact our support team.
Now pick a document for an advanced KYC :
{
"_id": "67da93c0dfd3a00f3380b857",
"type": "advanced",
"title": "Driving License",
"value": "DRIVERS_LICENSE",
"requiredFields": {
"first_name": {
"type": "string",
"label": "First Name",
"required": true
},
"last_name": {
"type": "string",
"label": "Last Name",
"required": true
},
"dob": {
"type": "date",
"label": "Date of birth",
"required": true
},
"email": {
"type": "email",
"label": "Email",
"required": true
},
"images": {
"type": "smile-identity-images",
"label": "Verification images",
"required": true
}
}
}
Everything is the same except the "images" field. It requires you to submit a photos of both sides of user's document and a user's selfie. Let's imagine that you took these photos and uploaded to the file storage under these URLs: https://cdn.com/selfie.jpg, https://cdn.com/front.jpg, https://cdn.com/back.jpg, the request to submit the KYC would look like the following:
{
"email": "[email protected]",
"countryIsoCode": "NG",
"documentId": "67da909b739fc481aa525c45",
"userFields": {
"first_name": "Joe",
"last_name": "Doe",
"dob": "2000-01-01T00:00:00.000Z",
"email": "[email protected]",
"images": [
{
"image_type_id": 0,
"image": "https://cdn.com/selfie.jpg"
},
{
"image_type_id": 1,
"image": "https://cdn.com/front.jpg"
},
{
"image_type_id": 5,
"image": "https://cdn.com/back.jpg"
}
]
},
}
The rest of the logic is the same
Creating an order
Let's get the best offer using parameters picked in previous steps, the order will be for 5 POLYGON USDC.
Request query:
?countryIsoCode=NG&paymentChannel=bank&network=POLYGON&asset=USDC&amount=5¤cy=crypto&includeRequiredFields=true
Response:
{
"quoteId": "687a45186848159c27269e38",
"offer": {
"countryIsoCode": "NG",
"currencyIsoCode": "NGN",
"paymentChannel": "bank",
"exchangeRate": 1532.89,
"cryptoExchangeRate": 1532.89
},
"cashout": {
"localCurrencyAmount": 7664,
"totalAmountUsd": 5,
"totalAmountCrypto": 5,
"withdrawAmountUsd": 5,
"withdrawAmountCrypto": 5,
"feePercent": 0,
"feeAmountUsd": 0,
"feeAmountLocalCurrency": 0,
"feeAmountCrypto": 0,
"feePercentFonbnk": 0,
"feeAmountUsdFonbnk": 0,
"feeAmountLocalCurrencyFonbnk": 0,
"feeAmountCryptoFonbnk": 0,
"feePercentPartner": 0,
"feeAmountUsdPartner": 0,
"feeAmountLocalCurrencyPartner": 0,
"feeAmountCryptoPartner": 0,
"gasAmountUsd": 0.00112894,
"gasAmountCrypto": 0,
"gasAmountLocalCurrency": 2
},
"requiredFields": {
"buyerFirstName": {
"label": "Your bank account first name",
"type": "string",
"required": true,
"sellerLabel": "Buyer first name"
},
"buyerLastName": {
"label": "Your bank account last name",
"type": "string",
"required": true,
"sellerLabel": "Buyer last name"
},
"buyerEmail": {
"label": "Your email",
"type": "email",
"required": true,
"sellerLabel": "Buyer email"
},
"bankCode": {
"required": true,
"type": "enum",
"label": "Bank name",
"sellerLabel": "Bank name",
"options": [
{
"value": "120001",
"label": "9mobile 9Payment Service Bank"
},
{
"value": "50871",
"label": "Unical MFB"
},
...
]
},
"phoneNumber": {
"label": "Your phone number",
"sellerLabel": "Buyer's phone number",
"type": "phone",
"required": true
}
}
}
From the response, we understand that a user should pay 7664 NGN to receive 5 POLYGON USDC, also we see that the next additional data is required from a user: buyerFirstName, buyerLastName, buyerEmail, bankCode, phoneNumber.
Let's create an order using all this data.
Request body:
{
"quoteId": "687a45186848159c27269e38",
"email": "[email protected]",
"network": "POLYGON",
"asset": "USDC",
"amount": 5,
"currency": "crypto",
"address": "0x91b0a33dbcb10f8331eD3627B94e5a9B1591269f",
"userIp": "145.234.234.55",
"redirectUrl": "https://your-website.con/fonbnk-success-page",
"extraFields": {
"buyerFirstName": "John",
"buyerLastName": "Doe",
"buyerEmail": "[email protected]",
"bankCode": "120001",
"phoneNumber": "234567890123",
},
}
Response:
{
"_id": "687a48eab6d730f80856e1ca",
"status": "swap_initiated",
"date": "2025-07-18T13:15:23.289Z",
"orderId": "687a48eab6d730f80856e1ca",
"email": "[email protected]",
"localCurrencyAmount": 7664,
"currencyIsoCode": "NGN",
"countryIsoCode": "NG",
"paymentChannel": "bank",
"amount": 5,
"amountCrypto": 5,
"network": "POLYGON",
"asset": "USDC",
"address": "0x91b0a33dbcb10f8331eD3627B94e5a9B1591269f",
"memo": null,
"orderParams": null,
"resumeUrl": "https://sandbox-pay.fonbnk.com/ussd/687a48eab6d730f80856e1ca",
"carrierId": "618e43914f57e07d255ff351",
"feePercent": 0,
"feePercentFonbnk": 0,
"feePercentPartner": 0,
"feeAmountUsd": 0,
"feeAmountLocalCurrency": 0,
"feeAmountUsdFonbnk": 0,
"feeAmountLocalCurrencyFonbnk": 0,
"feeAmountUsdPartner": 0,
"feeAmountLocalCurrencyPartner": 0,
"gasAmountUsd": 0.00112894,
"transferInstructions": {
"type": "manual",
"instructionsText": "It is a sandbox offer. If you are using test account, please confirm the transfer from your side and seller will automatically confirm the transfer from his side within 1 minute.",
"warningText": "Created orders from non-test accounts will be automatically canceled after 5 minutes.",
"transferDetails": {
"bankAccountNumber": {
"label": "Agent's bank account number",
"value": "5158809613"
},
"bankName": {
"label": "Agent's bank name",
"value": "Fidelity Bank"
},
"bankAccountHolderName": {
"label": "Agent's bank account holder name",
"value": "SANDY BOXERRITTO"
},
"narration": {
"label": "Narration",
"description": "Transfer without narration will be ignored by the system.",
"value": "6JTQAC"
}
}
},
"gasAmountLocalCurrency": 1
}
Order is created, now a user must pay to the agent using the "transferInstructions" details.
Here are possible types of transfer instructions:
Manual, a user pays manually to the provided details:
{
"transferInstructions": {
"type": "manual",
"transferDetails": {
"bankAccountNumber": {
"label": "Agent's bank account number",
"value": "9637959770"
},
"bankName": {
"label": "Agent's bank name",
"value": "PROVIDUS BANK"
},
"bankAccountHolderName": {
"label": "Agent's bank account holder name",
"value": "Start Button Limited(Checkout)"
},
"narration": {
"label": "Narration",
"description": "TRANSFER WITHOUT NARRATION WILL BE IGNORED BY THE SYSTEM.",
"value": "shc-0x6on2w5js"
}
},
"instructionsText": "Transfer the NGN to the agent's bank account.",
"warningText": "Important: Only transfer funds from a bank account you specified previously. Send the exact NGN amount. Use the displayed account for this transaction only."
}
}
STK Push, user receives a mobile carrier popup and confirms a transfer:
{
"transferInstructions": {
"type": "stk_push",
"instructionsText": "You’ll be prompted with a USSD dialog to proceed the transfer. If the transfer is unsuccessful or you don’t receive the USSD dialog, please retry the transfer"
}
}
Redirect, user must open a specidied redirect URL and complete the transfer there. After the success they will be redirected to the URL specified during order creation.
{
"transferInstructions": {
"type": "redirect",
"instructionsText": "You’ll be redirected to Flutterwave checkout. Enter the OTP code received to initiate the transaction. If you have any issues, please retry the transfer",
"paymentUrl": "https://checkout.flutterwave.com/captcha/verify/lang-en/9704816:565eee314972431349bd77403e57a741"
}
}
USSD, in that case, a user must execute the provided USSD code to complete the transfer. USSD code may include a {pin} placeholder; in that case you must ask the user to provide a pin code and replace the placeholder with it before execution.
{
"transferInstructions": {
"type": "ussd",
"ussdCode": "*321*0701232567*1587*{pin}#",
"transferDetails": {
"phoneNumber": {
"label": "Agent's phone number",
"value": "254701232567"
}
},
"instructionsText": "Dial the USSD code and follow the instructions to complete the transfer. You have to replace {pin} placeholder with your PIN code if you dial USSD code manually.",
"warningText": "Important: Transfer airtime NGN to the agent from the phone number you specified during order creation."
}
}
STK Push with OTP, in that case user receives an sms with an OTP code which must be sent to the confirm OTP endpoint , after that a user receives the STK Push and pays for the order
{
"transferInstructions": {
"type": "otp_stk_push",
"transferDetails": {},
"isOtpRequired": true,
"instructionsText": "Enter the OTP code received to initiate the transaction and you’ll be prompted with a USSD dialog to proceed the transfer. If the transfer is unsuccessful or you don’t receive the USSD dialog, please retry the transfer",
"actionButtonText": "Verify OTP code"
}
}
After a user sends funds to the agent, you must confirm the order.
Request body:
{
"orderId": "687a48eab6d730f80856e1ca"
}
The order flow is finished; now you need to wait for crypto to be sent to the user's wallet. You can do that either by waiting for a webhook or by fetching the order.
Last updated