# Server to server integration

{% hint style="warning" %}
Full server to server integration is possible only after a merchant KYB process
{% endhint %}

It's possible to do a full on-ramp flow by using only merchant API. [Here](/v1.5/reference/signing-requests.md) you can find how to send API requests correctly.\
\
The full workflow should look like this:

1. [Get a list of supported countries](/v1.5/endpoints/on-ramp.md#get-api-onramp-payment-channels) and their payment channels.
2. [Get a list of supported blockchain assets](/v1.5/endpoints/util.md#get-api-util-assets).
3. Pick a country, payment channel, and blockchain asset. [Get order limits](/v1.5/endpoints/on-ramp.md#get-api-onramp-limits) using these values.
4. [Get the user's KYC status](/v1.5/endpoints/user.md#post-api-user-kyc-status) and check if they need to pass a KYC process. If they need a KYC, [submit the document information](/v1.5/endpoints/user.md#post-api-user-kyc-submit) and [check the KYC status](/v1.5/endpoints/user.md#post-api-user-kyc-status) until it's accepted.
5. [Get the best offer](/v1.5/endpoints/on-ramp.md#get-api-onramp-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.
6. [Create an order](/v1.5/endpoints/on-ramp.md#post-api-onramp-order-create) 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](/v1.5/endpoints/on-ramp.md#post-api-onramp-order-otp).
7. [Confirm that a user sent funds to an agent](/v1.5/endpoints/on-ramp.md#post-api-onramp-order-confirm).

### Getting countries, payment channels, blockchain assets and order limits

Let's [get a list of supported countries](/v1.5/endpoints/on-ramp.md#get-api-onramp-payment-channels) , the response would be like this:

```json
[
  {
    "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](/v1.5/endpoints/util.md#get-api-util-assets):&#x20;

```json
[
  {
    "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](/v1.5/endpoints/on-ramp.md#get-api-onramp-limits) for Nigeria, bank payment channel and POLYGON USDC asset.\
Request query params:

```
?countryIsoCode=NG&paymentChannel=bank&network=POLYGON&asset=USDC
```

Response:

```json
{
  "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](/v1.5/endpoints/user.md#post-api-user-kyc-status) to determine if we need to sumbit a KYC documents.\
Request body:

```json
{
    email: "example@mail.com",
    countryIsoCode: "NG"
}
```

Response:

<pre class="language-json"><code class="lang-json">{
  "reachedKycLimit": false,
<strong>  "basicDocuments": [
</strong>    {
      "_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}$"
        }
      }
    },
<strong>    ...
</strong>  ],
  "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' },
    ],
  },
}

</code></pre>

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.&#x20;

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:

```json
{
      "_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:

```json
{
    "first_name": "Joe",
    "last_name": "Doe",
    "dob": "2000-01-01T00:00:00.000Z",
    "email": "example@mail.com",
    "id_number": "00000000000"
}
```

[Then we submit it](/v1.5/endpoints/user.md#post-api-user-kyc-submit) using the user's email, countryIsoCode, document ID, and these fields.<br>

Request body:

```json
{
      "email": "example@mail.com",
      "countryIsoCode": "NG",
      "documentId": "67da909b739fc481aa525c45",
      "userFields": {
        "first_name": "Joe",
        "last_name": "Doe",
        "dob": "2000-01-01T00:00:00.000Z",
        "email": "example@mail.com",
        "id_number": "00000000012",
      },
}

```

Let's [check a user's KYC status](/v1.5/endpoints/user.md#post-api-user-kyc-status) again:

```json
{
    "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:

```json
{
    "kycStatus": "initiated",
}
```

Failed KYC would look like the following:

```json
{
    "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 :

```json
{
      "_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:

```json
{
      "email": "example@mail.com",
      "countryIsoCode": "NG",
      "documentId": "67da909b739fc481aa525c45",
      "userFields": {
        "first_name": "Joe",
        "last_name": "Doe",
        "dob": "2000-01-01T00:00:00.000Z",
        "email": "example@mail.com",
        "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](/v1.5/endpoints/on-ramp.md#get-api-onramp-best-offer) using parameters picked in previous steps, the order will be for 5 POLYGON USDC.

Request query:

<pre data-overflow="wrap"><code><strong>?countryIsoCode=NG&#x26;paymentChannel=bank&#x26;network=POLYGON&#x26;asset=USDC&#x26;amount=5&#x26;currency=crypto&#x26;includeRequiredFields=true
</strong></code></pre>

Response:

```json
{
  "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.&#x20;

Let's [create an order](/v1.5/endpoints/on-ramp.md#post-api-onramp-order-create) using all this data.

Request body:

```json
{
  "quoteId": "687a45186848159c27269e38",
  "email": "example@mail.com",
  "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": "example@mail.com",
    "bankCode": "120001",
    "phoneNumber": "234567890123",
  },
}
```

Response:

```json
{
  "_id": "687a48eab6d730f80856e1ca",
  "status": "swap_initiated",
  "date": "2025-07-18T13:15:23.289Z",
  "orderId": "687a48eab6d730f80856e1ca",
  "email": "example@mail.com",
  "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:

```json
{
  "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:

```json
{
  "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.

```json
{
  "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.

```json
{
  "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](/v1.5/endpoints/on-ramp.md#post-api-onramp-order-otp) , after that a user receives the STK Push and pays for the order

```json
{
  "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](/v1.5/endpoints/on-ramp.md#post-api-onramp-order-confirm).

Request body:

```json
{
  "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](/v1.5/on-ramp/webhook.md) or by [fetching the order](/v1.5/endpoints/on-ramp.md#get-api-onramp-order).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.fonbnk.com/v1.5/on-ramp/server-to-server-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
