Xend Finance Merchant API Documentation
Swagger Documentation
For a complete and interactive API reference, including all endpoints, request/response models, and the ability to try out API calls directly, visit our Swagger documentation pages.
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.
Onboarding Steps
- Self-Register: Use the Register as a Merchant button above. Choose Sandbox for testing or Live for production.
- Verify Email: A verification email is sent to you with a token. Complete your registration by setting your password.
- Business KYC: In production, complete the automated business KYC verification via KYC Aid. Your company profile will display the KYC form URL when you first log in. In sandbox, KYC is automatically approved.
- Admin Approval (Production only): Once business KYC is approved, an admin will set your account status to
VERIFIEDto activate full access. - Configure Dashboard: Log in to the Xend Finance Dashboard, set up 2FA, webhook URL, wallet connect, and configure your API keys for access.
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. |
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 accounts for you (e.g., `WITHHOLDING_CUSTODIAL`) to hold and manage funds within the Xend Finance ecosystem.
[HMAC Session Credentials] For dashboard session operations (including proxy member management), authenticate with your merchant account. Login returns temporary session credentials used for HMAC signatures in subsequent requests.
[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 Proxy Member Sessions)
Use this method when calling member-scoped endpoints on behalf of a Proxy Member. It requires a temporary session token and secret obtained from the proxy member auth endpoint.
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 (Proxy Member Session-Based)
For proxy member requests, first call POST /Merchant/proxy/member/{proxyMemberId}/auth to get accessToken and accessSecret. Use the token as Bearer auth and generate x-hmac-signature from {timestamp}:{jsonBody} using the access secret.
// 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);
Proxy Member Sample: Get Member Profile
After proxy auth, call your member profile endpoint with the proxy member session credentials. Replace /Member/profile with your exact profile endpoint
from Swagger if it differs.
// Proxy member profile request (Node.js)
import crypto from 'crypto';
const accessToken = 'proxy_member_access_token_from_auth_endpoint';
const accessSecret = 'proxy_member_access_secret_from_auth_endpoint';
const bodyObject = {}; // GET profile usually has empty body
const payload = JSON.stringify(bodyObject);
const timestamp = Date.now().toString();
const signature = crypto
.createHmac('sha256', accessSecret)
.update(`${timestamp}:${payload}`)
.digest('hex');
const response = await fetch('https://api-solid-staging.xend.africa/Member/profile', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'x-request-timestamp': timestamp,
'x-hmac-signature': signature,
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
const profile = await response.json();
console.log(profile);
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.
Outbound Webhooks
Xend Finance sends real-time event notifications to a URL you configure in your merchant dashboard. Each request is signed with our platform RSA private key and includes your merchant webhook secret so you can authenticate it on receipt.
All deliveries are logged in your dashboard under Developer → Webhooks. From there you can inspect the JSON payload, response, and per-attempt status, and trigger manual resends.
Configuration
- Set your Webhook URL on the merchant dashboard (Settings → API & Webhooks). It must be HTTPS and publicly reachable.
- Copy your Webhook Secret — sent on every request as the
x-webhook-secretheader. - Copy the platform Webhook RSA Public Key — used to verify the
x-rsa-signatureheader. - Optionally pick which events to subscribe to via Manage Subscription. By default merchants receive all events; if you select a subset, events
outside that subset are recorded with delivery status
SKIPPEDanderrorMessage: "Event not in enabledWebhookEvents"— no HTTP request is sent.
Subscribing to Specific Events
The merchant company has an enabledWebhookEvents field with the following semantics:
null(default) — subscribed to all events.[]— subscribed to no events; nothing is delivered (everything is logged asSKIPPED).- Array of event keys — only the listed events are delivered. Valid keys come from the
merchantWebhookEventspublic enum.
Update the subscription via the merchant dashboard, or programmatically by sending the enabledWebhookEvents field on PUT /Merchant/lobby/company/profile/update.
Request Headers
| Header | Description |
|---|---|
| content-type | Always application/json. |
| x-webhook-event | Event name (see catalog below). |
| x-webhook-event-id | Unique id of this delivery. Use it for idempotency. |
| x-webhook-secret | Your merchant webhook secret. Compare with constant-time equality. |
| x-rsa-signature | Base64 RSA-SHA256 signature of the canonical request body. |
Payload Envelope
Every webhook body has the same top-level shape. The data object varies per event.
{
"event": "MEMBER_STATUS_CHANGED",
"eventId": "7fbc6e04-9d6c-4f7c-8e0e-1a1b2c3d4e5f",
"merchantCompanyId": "a3f1...",
"referenceId": "member-uuid",
"occurredAt": "2026-04-30T10:14:22.413Z",
"data": { "memberId": "...", "previousStatus": "ACTIVE", "newStatus": "SUSPENDED" }
}Verifying the Signature
- Read the raw request body as a UTF-8 string.
- Build a canonical string: re-serialize the parsed JSON with all object keys sorted alphabetically (recursively). This is what we sign on our side.
- Base64-decode the
x-rsa-signatureheader. - Verify with
RSA-SHA256using the platform Webhook Public Key. - Additionally, compare the
x-webhook-secretheader to your stored secret. - Respond
2xxwithin 15 seconds to acknowledge.
// Node.js (Express)
const crypto = require('crypto');
function canonicalize(value) {
return JSON.stringify(value, (_k, v) =>
v && typeof v === 'object' && !Array.isArray(v)
? Object.keys(v).sort().reduce((a, k) => (a[k] = v[k], a), {})
: v
);
}
app.post('/webhooks/xend', express.json({ limit: '1mb' }), (req, res) => {
if (req.get('x-webhook-secret') !== process.env.XEND_WEBHOOK_SECRET) {
return res.status(401).end();
}
const sig = Buffer.from(req.get('x-rsa-signature') || '', 'base64');
const ok = crypto.createVerify('RSA-SHA256')
.update(canonicalize(req.body))
.verify(process.env.XEND_WEBHOOK_PUBLIC_KEY, sig);
if (!ok) return res.status(400).end();
// Idempotency: store req.body.eventId; ignore duplicates.
// Process req.body.event + req.body.data
res.status(200).json({ received: true });
});Retries & Delivery Guarantees
- At-least-once delivery. Always de-duplicate using
eventId. - 3 attempts maximum. Initial attempt + retries at +1 minute, +10 minutes, +1 hour.
- Any non-2xx response or network/timeout error counts as a failure. After 3 attempts the delivery moves to
EXHAUSTED. - Request timeout: 15 seconds. Acknowledge first, process asynchronously.
- Manually resend any delivery from the dashboard. Resends re-sign the payload with the current platform key.
Event Catalog
| Event | When |
|---|---|
| MEMBER_CREATED | A new proxy member was created under your company. |
| MEMBER_UPDATED | A proxy member's profile fields changed. |
| MEMBER_STATUS_CHANGED | A proxy member's status changed (e.g. activated, suspended). |
| MEMBER_KYC_STATUS_CHANGED | KYC verification result for a member. |
| WALLET_CREDITED | A wallet under your company received funds. |
| WALLET_DEBITED | A wallet under your company was debited. |
| PROXY_TRANSFER_COMPLETED | A member-to-member transfer settled. |
| SWAP_COMPLETED | A token swap finished successfully. |
| SWAP_FAILED | A token swap failed. |
| FIAT_OFFRAMP_STATUS_CHANGED | Status update on a fiat off-ramp request. |
| BILL_PAYMENT_STATUS_CHANGED | Status update on a bill payment. |
| SAVINGS_DEPOSIT_COMPLETED | A savings deposit finalized. |
| SAVINGS_REDEEM_COMPLETED | A savings redemption finalized. |
| SAVINGS_INTEREST_PAID | Interest paid out on a savings position. |
Sample Payloads
Each example below is the full request body posted to your webhook URL. Field names inside data are stable; new optional fields may be added later, so parse
defensively.
MEMBER_CREATED
{
"event": "MEMBER_CREATED",
"eventId": "7fbc6e04-9d6c-4f7c-8e0e-1a1b2c3d4e5f",
"merchantCompanyId": "a3f1b2c4-...",
"referenceId": "mem_01HZX...",
"occurredAt": "2026-04-30T10:14:22.413Z",
"data": {
"memberId": "mem_01HZX...",
"externalRef": "merchant-user-123",
"email": "[email protected]",
"phoneNumber": "+2348012345678",
"firstName": "Jane",
"lastName": "Doe",
"status": "PENDING",
"createdAt": "2026-04-30T10:14:22.000Z"
}
}MEMBER_UPDATED
{
"event": "MEMBER_UPDATED",
"eventId": "...",
"merchantCompanyId": "a3f1...",
"referenceId": "mem_01HZX...",
"occurredAt": "2026-04-30T10:20:00.000Z",
"data": {
"memberId": "mem_01HZX...",
"changedFields": ["phoneNumber", "lastName"],
"phoneNumber": "+2348099999999",
"lastName": "Doe-Smith"
}
}MEMBER_STATUS_CHANGED
{
"event": "MEMBER_STATUS_CHANGED",
"eventId": "...",
"merchantCompanyId": "a3f1...",
"referenceId": "mem_01HZX...",
"occurredAt": "2026-04-30T10:25:00.000Z",
"data": {
"memberId": "mem_01HZX...",
"previousStatus": "ACTIVE",
"newStatus": "SUSPENDED",
"reason": "Suspended by merchant admin"
}
}MEMBER_KYC_STATUS_CHANGED
{
"event": "MEMBER_KYC_STATUS_CHANGED",
"eventId": "...",
"merchantCompanyId": "a3f1...",
"referenceId": "mem_01HZX...",
"occurredAt": "2026-04-30T10:30:00.000Z",
"data": {
"memberId": "mem_01HZX...",
"previousStatus": "SUBMITTED",
"newStatus": "APPROVED",
"submissionType": "STANDARD_KYC"
}
}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, or before any member-level request, first generate a new session token using the proxy member ID in the path.
Endpoint: POST /Merchant/proxy/member/{proxyMemberId}/auth
Proxy Member Auth Session
This is the recommended session flow for merchants integrating proxy members. It ensures your app always has a valid token and HMAC secret for member-level actions.
Session Flow
- Create the proxy member once:
POST /Merchant/proxy/member/add. - Store the returned
memberIdin your database and map it to your own user ID. - At login/session start, get a fresh proxy token:
POST /Merchant/proxy/member/{proxyMemberId}/auth. - Use the returned
accessTokenas Bearer token and sign every request using the returnedaccessSecret. - If you get
403 hmac_secret_missing, re-authenticate the proxy member and retry with the new session credentials.
See sample: Proxy member profile request with HMAC.
Important Behavior
Proxy member tokens are accepted on member-scoped endpoints. Internally, the token carries userType: ProxyMember, and the platform validates HMAC using the proxy member's
active access secret.
HMAC Formula
signature = HMAC_SHA256(accessSecret, "{timestamp}:{jsonBody}")
- x-request-timestamp: Must be current time in milliseconds.
- x-hmac-signature: Hex digest of the formula above.
- Body for GET: Use
{}when request body is empty.
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. |