Xend Finance Merchant API Documentation
Becoming a Merchant
Welcome to the Xend Finance Merchant API. Integrating with our powerful RESTful API and webhook service is the first step to leveraging our platform. The process begins with completing your business verification and setting up your account.
Onboarding Steps
- Complete KYB: Finalize the "Know Your Business" (KYB) process with our team.
- Provide Details: Submit your business account and SUPER admin profile information as structured below.
Merchant Data Structure
You will need to provide the following data. This Joi schema illustrates the required fields and their validation rules.
{
merchantCompany: Joi.object({
id: Joi.string().uuid(),
name: Joi.string(),
tagline: Joi.string(),
companyEmail: Joi.string().email().lowercase(),
countryCode: Joi.string().length(2),
logoUrl: Joi.string().uri().optional(),
websiteUrl: Joi.string().uri().optional(),
}),
merchantAccount: Joi.object({
email: Joi.string().email().lowercase(),
firstName: Joi.string(),
lastName: Joi.string(),
phoneNumber: Joi.string(),
countryCode: Joi.string().length(2),
}),
}
General API Response
All API responses follow a standardized JSON structure. This ensures consistency and predictability when you interact with our endpoints.
Response Object Structure
{
"data": {},
"status": "success",
"statusCode": 200,
"message": "Action Completed",
"action": null,
"messageLanguageCode": "app_001",
"details": null,
"cacheTTL": 20
}
Field Descriptions
| Field | Description |
|---|---|
| data | The main content returned by the API. Can be an object, array, or null. |
| status | Indicates the outcome of the request. Possible values are 'success' or 'failed'. |
| statusCode | The standard HTTP response status code. |
| message | A human-readable message to be displayed to the end-user (e.g., "Registration completed"). |
| messageLanguageCode | A unique code for the message, which can be used by frontend clients for translations. |
| action | A specific action the frontend should take, e.g., 'load_2fa_screen'. See action documentation for details. |
| details | Contains detailed debug messages or error logs for the developer. This field is not returned in production environments. |
| cacheTTL | Time-To-Live in seconds. Indicates how long the response is cached on the server. You may use this to manage your own caching logic. |
How It Works
Xend Finance Exchange offers a suite of DeFi features. To manage interactions, our system defines five core user types.
| User Type | Description |
|---|---|
| Administrator | The Xend Finance team. We manage the platform. |
| Merchant | This is you. A business integrated with our services. |
| Member | End-users on the Xend Finance platform. |
| ProxyMember | Your end-users, registered on Xend Finance via your integration. |
| Bot | Automated accounts for tasks like funding, cron jobs, and API auth. |
Account Setup & Configuration
[Registration & Activation] After KYB, we create your account and send an activation link to your `merchantAccount.email`. Follow it to access the Xend Finance Dashboard, complete registration, and set up 2FA.
[Webhook URL] In the dashboard, set your webhook URL. We send notifications here and often require acknowledgements.
[Wallet Connect String] Set a simple text string (like your business name) used for signing web3 messages, enabling instant funding verification.
[Custodial Accounts] We automatically create custodial bot accounts for you (e.g., `WITHHOLDING_CUSTODIAL`) to hold and manage funds within the Xend Finance ecosystem.
[HMAC BOT] For dashboard access and proxy member management, create an HMAC bot from the dashboard. This generates your API `username` and `password`, which you'll use for authenticating API requests. Store these credentials securely, as they are shown only once.
[RSA Public Key] For direct server-to-server API calls, you must generate an RSA key pair. In the Xend Finance Dashboard, upload your public key. This allows our servers to verify requests signed with your corresponding private key.
API Authentication
Our API uses a multi-layered authentication approach. The method you use depends on the type of action you are performing.
1. HMAC Signature (For Dashboard & Proxy Members)
This method is used for session-based activities, such as when you are logged into the dashboard or making API calls on behalf of a Proxy Member. It requires a temporary secret obtained after a login event.
2. RSA Signature (For Direct Merchant API)
This is a stateless method ideal for automated, server-to-server communication. It uses a public/private key pair and does not require a login session. All direct Merchant API calls (e.g., transfers, queries) must use RSA signatures.
HMAC Signature (Session-Based)
When a user (like a merchant admin) logs in, the API returns a temporary JWT (accessToken) and a corresponding HMAC key (accessSecret). For all subsequent requests within that session, you must use the `accessSecret` to generate an HMAC signature and include it in the x-hmac-signature header.
// Node.js Example
const crypto = require('crypto');
const generateHmacSignature = (secret, timestamp, payload) => {
const message = `${timestamp}:${payload}`;
return crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
};
const timestamp = Date.now();
const payload = JSON.stringify({ /* your request body */ });
const accessSecret = 'your_access_secret_from_login';
const hmacSignature = generateHmacSignature(accessSecret, timestamp, payload);
RSA Signature (Stateless Merchant API)
For direct server-to-server API calls, you must use RSA signing. This method is stateless and does not require a login session. It proves the request is from you and that the payload has not been tampered with.
RSA Authentication Flow
- Generate Key Pair: You must generate a standard RSA key pair. Keep your private key secret. See the section below for code samples.
- Upload Public Key: Upload your public key to the Xend Finance merchant dashboard.
- Construct String-to-Sign: Create a canonical string by sorting the keys in your JSON request payload and joining them (e.g., `amount=5000&destinationAccount=...`).
- Sign with Private Key: Use your private key to sign the string from the previous step using the `RSA-SHA256` algorithm.
- Send Headers: Make your API request with the following headers:
Authorization: `Bearer` x-api-key: Your global API key.x-rsa-signature: The Base64-encoded signature you just generated.
Verifying Your Uploaded Public Key
After uploading your public key you can prove that you still control the matching private key by calling POST /Merchant/lobby/company/public-key/verify. Sign a short metadata payload with your private key, send the signature to this endpoint, and we will reconstruct the same canonical string and verify it with the public key on file.
Verification Checklist
- Prepare Metadata: Include meaningful fields such as
companyName,contactEmail,submittedBy, an ISOtimestamp, and a uniquenonce. - Build Canonical String: Sort the metadata keys alphabetically, skip empty values, and join each entry as
key=valuewith&. - Sign with Private Key: Use
RSA-SHA256to sign the canonical string and Base64-encode the output. - Call the Verify Endpoint: POST the
metadataand Base64signatureto the endpoint. A success response confirms the signature matched your stored public key.
// verifyPublicKey.js
import crypto from 'crypto';
import fs from 'fs';
const metadata = {
companyName: 'Acme Pay Ltd',
contactEmail: '[email protected]',
submittedBy: '[email protected]',
timestamp: new Date().toISOString(),
nonce: `verify-${Date.now()}`,
};
const canonicalString = Object.keys(metadata)
.filter((key) => metadata[key] !== undefined && metadata[key] !== null && metadata[key] !== '')
.sort()
.map((key) => `${key}=${metadata[key]}`)
.join('&');
const privateKey = fs.readFileSync('./private_key.pem', 'utf8');
const signer = crypto.createSign('RSA-SHA256');
signer.update(canonicalString);
const signature = signer.sign(privateKey, 'base64');
console.log(JSON.stringify({ metadata, signature }, null, 2));
curl -X POST "https://api-solid-staging.xend.africa/api/Merchant/lobby/company/public-key/verify" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <MERCHANT_COMPANY_ID>" \
-H "x-api-key: <GLOBAL_API_KEY>" \
-d '{
"metadata": {
"companyName": "Acme Pay Ltd",
"contactEmail": "[email protected]",
"submittedBy": "[email protected]",
"timestamp": "2025-01-15T09:30:00.000Z",
"nonce": "2025-01-15T09:30:00Z#813472"
},
"signature": "<base64-signature-from-private-key>"
}'
Generating Your RSA Key Pair
To get started with RSA authentication, you first need to generate a 2048-bit RSA key pair. Run one of the scripts below to create a private_key.pem and public_key.pem. Keep the private key secure and upload the contents of the public key to your merchant dashboard.
// Save as generateKeys.js and run `node generateKeys.js`
const { generateKeyPair } = require('crypto');
const fs = require('fs');
generateKeyPair('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
}, (err, publicKey, privateKey) => {
if (err) { return console.error(err); }
fs.writeFileSync('public_key.pem', publicKey);
fs.writeFileSync('private_key.pem', privateKey);
console.log('Public and private key files generated!');
});RSA Signature Generation
⚠️ Important: Nested Object Handling
When your payload contains nested objects (like proxyKYCDetails),
you must convert them to JSON strings during signature generation. The server expects the exact same string format
for verification. See the examples below for proper implementation.
💡 Pro Tip: Include All Fields
Always include all required fields in your payload (even if they're false or null).
The server may add default values during validation, which will cause signature verification to fail if not included in your original payload.
// Node.js Example for RSA Signature
const crypto = require('crypto');
const fs = require('fs');
function generateRsaSignature(privateKey, payload) {
// Create the exact same sorted string from the payload.
// Filter out null/undefined values and handle nested objects.
const stringToSign = Object.keys(payload)
.filter(key => payload[key] != null && payload[key] !== '')
.sort()
.map(key => {
const value = payload[key];
// Handle nested objects by converting them to JSON strings
if (typeof value === 'object' && value !== null) {
return `${key}=${JSON.stringify(value)}`;
}
return `${key}=${value}`;
})
.join('&');
// Sign the string with your private key.
const signature = crypto
.createSign('RSA-SHA256')
.update(stringToSign)
.sign(privateKey, 'base64');
return signature;
}
// Example Usage:
const privateKey = fs.readFileSync('path/to/your_private_key.pem', 'utf-8');
const requestPayload = {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
countryCode: 'NG',
phoneNumber: '08012345678',
kycStandard: false,
kycProofOfAddress: false,
kycBusinessVerification: false,
proxyKYCDetails: {
idCardType: 'PASSPORT',
idCardNumber: 'A12345678',
countryOfIssue: 'NG'
}
};
// Generate nonce for header
const nonceStr = crypto.randomBytes(16).toString('hex');
const rsaSignature = generateRsaSignature(privateKey, requestPayload);
// Send this rsaSignature in the 'x-rsa-signature' header.
Verifying Webhooks
To ensure the security and integrity of data sent to your webhook URL, all outgoing requests from Xend Finance are signed. We include a signature in the request body. You must verify this signature using the Xend Finance Public Key, which is available for you to copy from your merchant dashboard.
This process guarantees that the webhook was sent by Xend Finance and that its content has not been altered.
Webhook Verification Flow
- Get Public Key: Copy the Xend Finance Public Key from your merchant dashboard.
- Receive Webhook: Your server receives a POST request at your configured webhook URL.
- Extract Signature: Remove the `signature` field from the webhook's JSON payload.
- Construct String-to-Verify: Create a canonical string from the rest of the payload by sorting the keys alphabetically and joining them (e.g., `amount=5000&status=success...`).
- Verify Signature: Use the Xend Finance Public Key to verify that the extracted signature matches the string you constructed. If it's valid, process the webhook. Otherwise, discard the request.
Important: Handling Nested Objects
⚠️ Critical for Webhook Verification:
When constructing the string-to-verify for webhook signature validation, you must handle nested objects (objects and arrays) by converting them to JSON strings using JSON.stringify() (JavaScript), json.dumps() (Python), json_encode() (PHP), or JsonConvert.SerializeObject() (C#).
Example: If your payload contains {"user": {"id": 123, "name": "John"}, "amount": 500}, the string-to-verify should be amount=500&user={"id":123,"name":"John"}
Webhook Verification Examples
// Node.js Example for Webhook Verification
const crypto = require('crypto');
function verifyWebhookSignature(publicKey, payload) {
const { signature, ...dataToVerify } = payload;
if (!signature) return false;
const stringToVerify = Object.keys(dataToVerify)
.filter(key => dataToVerify[key] != null && dataToVerify[key] !== '')
.sort()
.map(key => {
const value = dataToVerify[key];
if (typeof value === 'object' && value !== null) {
return `${key}=${JSON.stringify(value)}`;
}
return `${key}=${value}`;
})
.join('&');
const isVerified = crypto
.createVerify('RSA-SHA256')
.update(stringToVerify)
.verify(publicKey, signature, 'base64');
return isVerified;
}
// Example Usage in an Express.js route
app.post('/webhook', (req, res) => {
const xendPublicKey = '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----'; // From your dashboard
const isValid = verifyWebhookSignature(xendPublicKey, req.body);
if (isValid) {
console.log('Webhook verified successfully.');
res.status(200).send('OK');
} else {
console.error('Webhook verification failed.');
res.status(400).send('Invalid signature');
}
});HTTPS Request Samples
Below are sample code examples showing how to make authenticated HTTPS requests to the Merchant API. These examples demonstrate the proper headers and authentication flow required for all merchant endpoints using RSA signatures.
Required Headers
All merchant API requests require the following headers for authentication and security:
| Header | Type | Description | Example |
|---|---|---|---|
| Accept | string | Content type for the response | application/json |
| x-api-key | string | API key for general authorization | your_api_key_here |
| Authorization | string | Bearer token containing your merchant ID | Bearer your_merchant_id |
| x-rsa-signature | string | Base64-encoded RSA signature for request integrity | generated_rsa_signature |
| x-country-code | string | ISO alpha-2 country code or "WORLD" for all countries | NG or WORLD |
Request Sample Code
Here's a complete example of how to make authenticated requests to the Merchant API using RSA signatures:
// Node.js Example - Merchant API Request with RSA Signature
const crypto = require('crypto');
const fs = require('fs');
const axios = require('axios');
const generateRsaSignature = (privateKey, payload) => {
// Create the exact same sorted string from the payload
const stringToSign = Object.keys(payload)
.filter(key => payload[key] != null && payload[key] !== '')
.sort()
.map(key => `${key}=${payload[key]}`)
.join('&');
// Sign the string with your private key
const signature = crypto
.createSign('RSA-SHA256')
.update(stringToSign)
.sign(privateKey, 'base64');
return signature;
};
const makeMerchantRequest = async (
baseUrl,
method,
url,
payload,
merchantId,
apiKey,
privateKey,
countryCode = 'WORLD'
) => {
try {
const rsaSignature = generateRsaSignature(privateKey, payload);
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'x-api-key': apiKey,
'Authorization': `Bearer ${merchantId}`,
'x-rsa-signature': rsaSignature,
'x-country-code': countryCode
};
const config = {
method: method.toLowerCase(),
url: `${baseUrl}${url}`,
headers: headers,
data: method.toLowerCase() !== 'get' ? payload : undefined
};
const response = await axios(config);
return response.data;
} catch (error) {
console.error(`Error during request to ${url}:`, error.response?.data || error.message);
throw error;
}
};
// Example usage
const exampleRequest = async () => {
const privateKey = fs.readFileSync('path/to/your_private_key.pem', 'utf-8');
const merchantId = 'your_merchant_id';
const apiKey = 'your_api_key';
const baseUrl = 'https://api-solid-staging.xend.africa';
// Example: Get merchant profile
const payload = {
requestTime: Date.now()
};
// Generate nonce for header
const nonceStr = crypto.randomBytes(16).toString('hex');
const response = await makeMerchantRequest(
baseUrl,
'GET',
'/api/Merchant/profile',
payload,
merchantId,
apiKey,
privateKey,
'NG'
);
console.log('Response:', response);
};
Header Explanations
Authentication Headers
- x-api-key: General API authorization key required for all requests
- Authorization: Bearer token containing your merchant ID
- x-rsa-signature: Base64-encoded RSA signature for request integrity verification
- x-request-timestamp: Current timestamp in milliseconds for request freshness
- x-nonce-string: Unique random string for each request to prevent replay attacks
Security & Context Headers
- x-country-code: ISO alpha-2 country code or "WORLD" for filtering data
- Accept: Specifies the expected response format (application/json)
- Content-Type: Specifies the request body format (application/json)
Proxy Members
Proxy Members are your users, registered and managed on Xend Finance through the API. This allows you to build a seamless experience where your users can interact with Xend Finance's features without leaving your application. API calls for proxy members use the session-based HMAC authentication method.
Register a Proxy Member
Use this endpoint to create a new user on Xend Finance under your merchant account. The response will include an `accessToken` and `accessSecret` specific to that user, which you can use to make API calls on their behalf.
Important: Only the externalProxyMemberUniqueId field is required. This is the unique identifier for the proxy member in your own merchant system and cannot be changed after creation. All other fields are optional.
Endpoint: POST /Merchant/proxymember/add
Sample Response
{
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"accessSecret": "random-secret-string",
"sessionExpiryTime": "2h",
"memberId": "64f1b1b1b1b1b1b1b1b1b1b1",
"mid": 20123456,
"email": "uniqueID123@[email protected]",
"profile": { ... }
}
}
Refresh Proxy Member Token
When a proxy member's token expires, use this endpoint to generate a new one using their `memberId`.
Endpoint: POST /Merchant/proxymember/auth
Member Proxy Transfers
This feature is crucial for managing user balances. It allows you to move funds between your main custodial wallet (e.g., `WITHHOLDING_CUSTODIAL`) and the wallets of your proxy members. This enables you to sync balances between your local application and the Xend Finance platform.
Example Flow:
- A user wants to move funds from their wallet in your app to their Xend Finance trading wallet.
- You debit the user's balance in your local database.
- You call the Proxy Funds Transfer endpoint to credit the corresponding proxy member's wallet on Xend Finance.
Endpoint: POST /Merchant/proxyfunds/transfer
POS Agent - Crypto Payment Processing
POS (Point of Sale) Agents are proxy members who can receive cryptocurrency payments from customers and provide fiat cash in return. This feature enables On-Ramp/Off-Ramp processes where customers can pay with crypto and receive fiat currency.
Complete POS Agent Workflow
🔄 POS Agent Payment Flow Diagram
Setup Supported Currencies
Merchant configures supported cryptocurrencies and fiat currencies for POS operations
Create Proxy Member
Merchant hits API endpoint to create proxy member account
Configure POS Agent
Edit proxy member to make them POS agent and assign staff permissions
Generate Invoice
Use /validate and /process endpoints to create crypto payment invoice
Display Invoice
Show invoice details to both POS Agent and Customer
Customer Pays Crypto
Customer sends cryptocurrency to provided wallet address
Manual Confirmation (Optional)
POS Agent clicks "I have paid" if status doesn't change automatically
Confirm Payment
POS Agent confirms payment by checking status or scanning transaction ID from customer device
Complete Transaction
POS Agent pays cash to customer (On-Ramp) or Xend Finance sends crypto to customer (Off-Ramp)
📋 Detailed Step-by-Step Flow
Step 0: Setup Supported Currencies
Merchant configures which cryptocurrencies and fiat currencies are supported for POS operations in their merchant dashboard.
Step 1: Create Proxy Member
Merchant hits the API endpoint POST /Merchant/proxy/member/add to create a new proxy member account.
Step 2: Configure POS Agent
Use the edit proxy member endpoint PUT /Merchant/proxy/member/update/{proxyMemberId} to:
- Set
isPosAgent: true - Assign staff permissions and roles
- Configure POS agent profile details
Step 3: Generate Invoice
POS Agent creates invoice using the validation and processing endpoints:
POST /Merchant/pos/invoice/crypto/{proxyMemberId}/validate- Validate invoice detailsPOST /Merchant/pos/invoice/crypto/{proxyMemberId}/process- Process and generate payment instructions
Step 4: Display Invoice
Merchant UI displays the invoice details to both:
- POS Agent: Invoice management interface
- Customer: Payment instructions (wallet address, amount, network)
Step 5: Customer Pays Crypto
Customer sends cryptocurrency from their wallet to the provided wallet address using the specified blockchain network.
Step 6: Manual Confirmation (Optional)
If the payment status doesn't change automatically, POS Agent can:
- Click "I have paid" button in the merchant UI
- Use
POST /Merchant/pos/invoice/{proxyMemberId}/{invoiceId}/transaction-hash
Step 7: Confirm Payment
POS Agent confirms payment by:
- Checking payment status in the merchant UI
- Scanning transaction ID from customer's device
- Verifying blockchain transaction details
Step 8: Complete Transaction
Final step depends on transaction type:
- On-Ramp (Crypto → Fiat): POS Agent pays cash to customer after confirming crypto receipt
- Off-Ramp (Fiat → Crypto): Xend Finance automatically sends crypto to customer (feature coming soon)
POS Invoice Payment Workflow
Step 1: Validate Invoice
Endpoint: POST /Merchant/pos/invoice/crypto/{proxyMemberId}/validate
Validate the invoice details (amount, currency, invoice ID) before processing crypto payment.
- POS agent creates invoice in merchant UI
- Customer chooses crypto payment option
- System validates invoice details
- Returns validation result
Step 2: Process Payment
Endpoint: POST /Merchant/pos/invoice/crypto/{proxyMemberId}/process
Initiate the crypto payment process and generate payment instructions.
- System generates payment instructions (wallet address, amount, network)
- Customer receives payment details
- Customer sends crypto from their wallet
- System monitors blockchain for payment
Step 3: Confirm Payment (Optional)
Endpoint: POST /Merchant/pos/invoice/{proxyMemberId}/{invoiceId}/transaction-hash
Manually submit transaction hash for faster confirmation.
- Optional: System automatically detects payments
- Customer provides transaction hash to POS agent
- POS agent clicks "I have paid" button in UI
- Faster confirmation than waiting for automatic detection
On-Ramp/Off-Ramp Process
This system enables a complete On-Ramp/Off-Ramp process where customers can exchange crypto for fiat currency:
On-Ramp (Crypto → Fiat)
- Customer pays with cryptocurrency
- POS agent receives crypto payment
- POS agent provides fiat cash to customer
- Customer gets fiat currency
Off-Ramp (Fiat → Crypto)
- Customer pays with fiat currency
- POS agent receives fiat payment
- POS agent sends crypto to customer
- Customer gets cryptocurrency
Integration Requirements
Merchant Responsibilities:
- Provide UI interface for POS agents
- Handle POS agent authentication and session management
- Send API requests to Xend Finance on behalf of POS agents
- Display payment instructions to customers
- Handle transaction hash submission from POS agents
Xend Finance Handles:
- Invoice validation and processing
- Payment instruction generation
- Blockchain transaction monitoring
- Automatic payment detection
- Transaction hash verification
💡 Pro Tip
The transaction hash submission endpoint is optional but recommended for faster payment confirmation. The system automatically detects payments on the blockchain, but manual submission provides immediate confirmation when customers provide their transaction hash.
Pagination
Endpoints that return a list of items are paginated. You can control the pagination using the following parameters in your request payload.
| Parameter | Type | Description |
|---|---|---|
| pageId | number | The page number you want to retrieve. Starts at 1. |
| perPage | number | The number of items to return per page. |
| sort | string | The sort order. Accepts ASC for ascending or DESC for descending. |
Swagger Documentation
For a complete and interactive API reference, including all endpoints, request/response models, and the ability to try out API calls directly, please visit our Swagger documentation pages.