> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bafanglaicai88.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Describe only the public 123hub merchant API documented on this site. The primary endpoint is POST /public/api/multihub/v1.
> Preserve API method names, field names, header names, and error codes exactly as documented. Do not invent endpoints or parameters.
> Treat every amount as an integer in minor units unless a page explicitly states otherwise.
> Never expose, request, or fabricate a merchant secret key. The SDK pages contain reference implementations, not official SDK packages.

# Node.js Examples

> Integrate with the 123hub API using Node.js

# Node.js Examples

Complete Node.js example client for integrating with the 123hub API using the built-in `crypto` module and native `fetch`.

<Info>
  This is a reference implementation, not an official SDK package. It uses only Node.js built-in
  modules (no external dependencies required). Requires Node.js 18+ for native `fetch`.
</Info>

## Configuration

Store your credentials in environment variables:

```bash theme={null}
export PAYMENT_API_APP_ID="1"
export PAYMENT_API_VERSION="2"
export PAYMENT_API_SECRET_KEY="your_secret_key"
```

## PaymentApiClient Class (JavaScript)

```javascript theme={null}
const crypto = require('crypto');

class PaymentApiClient {
  constructor({
    appId = process.env.PAYMENT_API_APP_ID,
    apiVersion = Number(process.env.PAYMENT_API_VERSION || '1'),
    secretKey = process.env.PAYMENT_API_SECRET_KEY,
    baseUrl = 'https://api.bafanglaicai88.com',
    timeoutMs = 30000,
  } = {}) {
    if (!appId || !secretKey) {
      throw new Error('appId and secretKey are required');
    }
    this.appId = appId;
    if (![1, 2].includes(apiVersion)) throw new Error('apiVersion must be 1 or 2');
    this.apiVersion = apiVersion;
    this.secretKey = secretKey;
    this.baseUrl = baseUrl;
    this.timeoutMs = timeoutMs;
  }

  async _send(body) {
    const bodyStr = JSON.stringify(body);
    const path = `/public/api/multihub/v${this.apiVersion}`;
    const headers = {
      'Content-Type': 'application/json',
      'X-Data-Application-Id': String(this.appId),
    };
    let requestNonce;
    if (this.apiVersion === 1) {
      headers['X-Data-Hash'] = crypto
        .createHash('sha512')
        .update(bodyStr + this.secretKey)
        .digest('hex');
    } else {
      const timestamp = Math.floor(Date.now() / 1000).toString();
      const nonce = crypto.randomBytes(24).toString('hex');
      requestNonce = nonce;
      const canonical = [
        'quadpay-multihub-request-v2',
        String(this.appId),
        'POST',
        '/public/api/multihub/v2',
        timestamp,
        nonce,
        crypto.createHash('sha256').update(bodyStr).digest('hex'),
      ].join('\n');
      headers['X-Data-Timestamp'] = timestamp;
      headers['X-Data-Nonce'] = nonce;
      headers['X-Data-Signature'] = crypto
        .createHmac('sha512', this.secretKey)
        .update(canonical)
        .digest('hex');
    }

    const response = await fetch(this.baseUrl + path, {
      method: 'POST',
      headers,
      body: bodyStr,
      signal: AbortSignal.timeout(this.timeoutMs),
    });
    const responseText = await response.text();
    const parsed = JSON.parse(responseText);
    if (this.apiVersion === 2) {
      const requestId = response.headers.get('x-request-id') || parsed.request_id;
      const canonicalResponse = [
        'quadpay-multihub-response-v2',
        String(this.appId),
        requestNonce,
        requestId,
        String(response.status),
        crypto.createHash('sha256').update(responseText).digest('hex'),
      ].join('\n');
      const expected = crypto
        .createHmac('sha512', this.secretKey)
        .update(canonicalResponse)
        .digest('hex');
      const received = response.headers.get('x-data-signature') || '';
      if (
        !requestId ||
        !/^[0-9a-f]{128}$/.test(received) ||
        !crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'))
      ) {
        throw new Error('Merchant API v2 response signature verification failed');
      }
    }
    return parsed;
  }

  async ping() {
    return this._send({ method: 'gateway.ping', params: {} });
  }

  async createDeposit(serviceId, amount, currency, cId, extra = {}) {
    return this._send({
      method: 'payment.in',
      service_id: serviceId,
      params: {
        payment: {
          identifiers: { c_id: cId },
          amount: { value: amount, currency },
          ...extra,
        },
      },
    });
  }

  async createWithdrawal(serviceId, amount, currency, cId, receiver, extra = {}) {
    return this._send({
      method: 'payment.out',
      service_id: serviceId,
      params: {
        payment: {
          identifiers: { c_id: cId },
          amount: { value: amount, currency },
          receiver,
          ...extra,
        },
      },
    });
  }

  async getStatus({ cId, hId } = {}) {
    const identifiers = {};
    if (cId) identifiers.c_id = cId;
    if (hId) identifiers.h_id = hId;
    return this._send({
      method: 'payment.status',
      params: { payment: { identifiers } },
    });
  }

  async getBalance() {
    return this._send({ method: 'balance.get', params: {} });
  }
}

module.exports = { PaymentApiClient };
```

## PaymentApiClient Class (TypeScript)

```typescript theme={null}
import crypto from 'crypto';

interface PaymentApiResponse {
  success: boolean;
  result?: Record<string, any>;
  error?: { code: number; message: string };
  request_id: string;
  processing_time: number;
}

interface PaymentApiClientOptions {
  appId?: string;
  apiVersion?: 1 | 2;
  secretKey?: string;
  baseUrl?: string;
  timeoutMs?: number;
}

interface Receiver {
  bank?: {
    account: { id: string };
    ifsc?: string;
  };
  email?: string;
  phone?: string;
  person?: { first_name: string; last_name: string };
  [key: string]: any;
}

class PaymentApiClient {
  private appId: string;
  private apiVersion: 1 | 2;
  private secretKey: string;
  private baseUrl: string;
  private timeoutMs: number;

  constructor(options: PaymentApiClientOptions = {}) {
    this.appId = options.appId || process.env.PAYMENT_API_APP_ID || '';
    this.apiVersion =
      options.apiVersion || (Number(process.env.PAYMENT_API_VERSION || '1') as 1 | 2);
    this.secretKey = options.secretKey || process.env.PAYMENT_API_SECRET_KEY || '';
    this.baseUrl = options.baseUrl || 'https://api.bafanglaicai88.com';
    this.timeoutMs = options.timeoutMs || 30000;

    if (!this.appId || !this.secretKey) {
      throw new Error('appId and secretKey are required');
    }
    if (![1, 2].includes(this.apiVersion)) {
      throw new Error('apiVersion must be 1 or 2');
    }
  }

  private async send(body: Record<string, any>): Promise<PaymentApiResponse> {
    const bodyStr = JSON.stringify(body);
    const path = `/public/api/multihub/v${this.apiVersion}`;
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'X-Data-Application-Id': this.appId,
    };
    let requestNonce: string | undefined;

    if (this.apiVersion === 1) {
      headers['X-Data-Hash'] = crypto
        .createHash('sha512')
        .update(bodyStr + this.secretKey)
        .digest('hex');
    } else {
      const timestamp = Math.floor(Date.now() / 1000).toString();
      const nonce = crypto.randomBytes(24).toString('hex');
      requestNonce = nonce;
      const canonical = [
        'quadpay-multihub-request-v2',
        this.appId,
        'POST',
        '/public/api/multihub/v2',
        timestamp,
        nonce,
        crypto.createHash('sha256').update(bodyStr).digest('hex'),
      ].join('\n');
      headers['X-Data-Timestamp'] = timestamp;
      headers['X-Data-Nonce'] = nonce;
      headers['X-Data-Signature'] = crypto
        .createHmac('sha512', this.secretKey)
        .update(canonical)
        .digest('hex');
    }

    const response = await fetch(this.baseUrl + path, {
      method: 'POST',
      headers,
      body: bodyStr,
      signal: AbortSignal.timeout(this.timeoutMs),
    });
    const responseText = await response.text();
    const parsed = JSON.parse(responseText) as PaymentApiResponse & { request_id?: string };
    if (this.apiVersion === 2) {
      const requestId = response.headers.get('x-request-id') || parsed.request_id;
      const canonicalResponse = [
        'quadpay-multihub-response-v2',
        this.appId,
        requestNonce,
        requestId,
        String(response.status),
        crypto.createHash('sha256').update(responseText).digest('hex'),
      ].join('\n');
      const expected = crypto
        .createHmac('sha512', this.secretKey)
        .update(canonicalResponse)
        .digest('hex');
      const received = response.headers.get('x-data-signature') || '';
      if (
        !requestId ||
        !/^[0-9a-f]{128}$/.test(received) ||
        !crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'))
      ) {
        throw new Error('Merchant API v2 response signature verification failed');
      }
    }
    return parsed;
  }

  async ping(): Promise<PaymentApiResponse> {
    return this.send({ method: 'gateway.ping', params: {} });
  }

  async createDeposit(
    serviceId: number,
    amount: number,
    currency: string,
    cId: string,
    extra: Record<string, any> = {},
  ): Promise<PaymentApiResponse> {
    return this.send({
      method: 'payment.in',
      service_id: serviceId,
      params: {
        payment: {
          identifiers: { c_id: cId },
          amount: { value: amount, currency },
          ...extra,
        },
      },
    });
  }

  async createWithdrawal(
    serviceId: number,
    amount: number,
    currency: string,
    cId: string,
    receiver: Receiver,
    extra: Record<string, any> = {},
  ): Promise<PaymentApiResponse> {
    return this.send({
      method: 'payment.out',
      service_id: serviceId,
      params: {
        payment: {
          identifiers: { c_id: cId },
          amount: { value: amount, currency },
          receiver,
          ...extra,
        },
      },
    });
  }

  async getStatus(options: { cId?: string; hId?: string } = {}): Promise<PaymentApiResponse> {
    const identifiers: Record<string, string> = {};
    if (options.cId) identifiers.c_id = options.cId;
    if (options.hId) identifiers.h_id = options.hId;
    return this.send({
      method: 'payment.status',
      params: { payment: { identifiers } },
    });
  }

  async getBalance(): Promise<PaymentApiResponse> {
    return this.send({ method: 'balance.get', params: {} });
  }
}

export { PaymentApiClient, PaymentApiResponse, PaymentApiClientOptions };
```

## Usage Examples

### Ping (Health Check)

```javascript theme={null}
const { PaymentApiClient } = require('./payment-api');

const client = new PaymentApiClient({
  appId: '1',
  apiVersion: 2,
  secretKey: 'your_secret_key',
});

async function main() {
  const result = await client.ping();
  console.log(result);
  // { success: true, result: { message: "pong" }, ... }
}

main();
```

### Create a Deposit Payment

The examples below use INR (India), but the same client works for all supported currencies (MXN, ARS, TRY, AUD, LKR, UYU) -- just change the `serviceId`, `currency`, and `country` values.

```javascript theme={null}
const result = await client.createDeposit(
  14701, // service_id
  10000, // amount
  'INR', // currency
  '12345', // c_id (your client-side identifier)
  {
    description: 'Order #12345',
    payer: {
      email: 'customer@example.com',
      person: { first_name: 'John', last_name: 'Doe' },
    },
  },
);

if (result.success) {
  console.log('Payment created successfully');
  console.log('Request ID:', result.request_id);
} else {
  console.error(`Error: ${result.error.code} - ${result.error.message}`);
}
```

### Create a Withdrawal (Payout)

```javascript theme={null}
const result = await client.createWithdrawal(
  14701, // service_id
  5000, // amount
  'INR', // currency
  '67890', // c_id
  {
    // receiver
    bank: {
      account: { id: '1234567890' },
      ifsc: 'SBIN0001234',
    },
    person: { first_name: 'Jane', last_name: 'Doe' },
  },
  {
    description: 'Payout #67890',
  },
);

if (result.success) {
  console.log('Withdrawal created successfully');
} else {
  console.error(`Error: ${result.error.code} - ${result.error.message}`);
}
```

### Check Payment Status

```javascript theme={null}
// Look up by client-side ID
const result = await client.getStatus({ cId: '12345' });

if (result.success) {
  const status = result.result.payment.status.status;
  console.log(`Payment status: ${status}`);
} else {
  console.error(`Error: ${result.error.code} - ${result.error.message}`);
}

// Look up by hub-side ID
const result2 = await client.getStatus({ hId: '999001' });
```

### Get Balance

```javascript theme={null}
const result = await client.getBalance();

if (result.success) {
  for (const amount of result.result.balance.amounts) {
    console.log(`${amount.currency}: ${amount.value}`);
  }
} else {
  console.error(`Error: ${result.error.code} - ${result.error.message}`);
}
```

## Error Handling

Successful responses return HTTP 200, errors return HTTP 400. Always use the `success` field to determine the outcome:

```javascript theme={null}
const result = await client.createDeposit(14701, 10000, 'INR', '12345');

if (result.success) {
  // Operation succeeded
  console.log(`Request ID: ${result.request_id}`);
  console.log(`Processing time: ${result.processing_time}ms`);
} else {
  // Operation failed
  const { code, message } = result.error;
  console.error(`Error code: ${code}`);
  console.error(`Error message: ${message}`);

  switch (code) {
    case 3000:
      console.error('Authentication failed - check your secret key');
      break;
    case 1005:
      console.error('Invalid parameters - check request body');
      break;
    case 7002:
      console.error('Service temporarily unavailable - retry later');
      break;
    default:
      console.error(`Unhandled error code: ${code}`);
  }
}
```

## Handling Network Errors

Network-level errors can still occur independently of API responses:

```javascript theme={null}
const { PaymentApiClient } = require('./payment-api');

const client = new PaymentApiClient({
  appId: '1',
  apiVersion: 2,
  secretKey: 'your_secret_key',
});

async function safePing() {
  try {
    const result = await client.ping();
    if (result.success) {
      console.log('API is reachable');
    } else {
      console.error(`API error: ${result.error.message}`);
    }
  } catch (error) {
    if (error.cause?.code === 'ECONNREFUSED') {
      console.error('Cannot reach the API - check network connectivity');
    } else if (error.name === 'AbortError') {
      console.error('Request timed out - try again');
    } else {
      console.error(`Request failed: ${error.message}`);
    }
  }
}
```

## Complete Example

```javascript theme={null}
const crypto = require('crypto');

class PaymentApiClient {
  constructor({
    appId,
    apiVersion,
    secretKey,
    baseUrl = 'https://api.bafanglaicai88.com',
    timeoutMs = 30000,
  }) {
    if (![1, 2].includes(apiVersion)) {
      throw new Error('apiVersion must be 1 or 2');
    }
    this.appId = appId;
    this.apiVersion = apiVersion;
    this.secretKey = secretKey;
    this.baseUrl = baseUrl;
    this.timeoutMs = timeoutMs;
  }

  async _send(body) {
    const bodyStr = JSON.stringify(body);
    const path = `/public/api/multihub/v${this.apiVersion}`;
    const headers = {
      'Content-Type': 'application/json',
      'X-Data-Application-Id': this.appId,
    };
    if (this.apiVersion === 1) {
      headers['X-Data-Hash'] = crypto
        .createHash('sha512')
        .update(bodyStr + this.secretKey)
        .digest('hex');
    } else {
      const timestamp = Math.floor(Date.now() / 1000).toString();
      const nonce = crypto.randomBytes(24).toString('hex');
      const canonical = [
        'quadpay-multihub-request-v2',
        this.appId,
        'POST',
        '/public/api/multihub/v2',
        timestamp,
        nonce,
        crypto.createHash('sha256').update(bodyStr).digest('hex'),
      ].join('\n');
      headers['X-Data-Timestamp'] = timestamp;
      headers['X-Data-Nonce'] = nonce;
      headers['X-Data-Signature'] = crypto
        .createHmac('sha512', this.secretKey)
        .update(canonical)
        .digest('hex');
    }
    const response = await fetch(this.baseUrl + path, {
      method: 'POST',
      headers,
      body: bodyStr,
      signal: AbortSignal.timeout(this.timeoutMs),
    });
    const responseText = await response.text();
    if (this.apiVersion === 2) {
      const parsed = JSON.parse(responseText);
      const requestId = response.headers.get('x-request-id') || parsed.request_id;
      const canonicalResponse = [
        'quadpay-multihub-response-v2',
        this.appId,
        headers['X-Data-Nonce'],
        requestId,
        String(response.status),
        crypto.createHash('sha256').update(responseText).digest('hex'),
      ].join('\n');
      const expected = crypto
        .createHmac('sha512', this.secretKey)
        .update(canonicalResponse)
        .digest('hex');
      const received = response.headers.get('x-data-signature') || '';
      if (
        !/^[0-9a-f]{128}$/.test(received) ||
        !requestId ||
        !crypto.timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'))
      ) {
        throw new Error('Merchant API v2 response signature verification failed');
      }
      return parsed;
    }
    return JSON.parse(responseText);
  }

  async ping() {
    return this._send({ method: 'gateway.ping', params: {} });
  }
  async getBalance() {
    return this._send({ method: 'balance.get', params: {} });
  }

  async createDeposit(serviceId, amount, currency, cId, extra = {}) {
    return this._send({
      method: 'payment.in',
      service_id: serviceId,
      params: {
        payment: {
          identifiers: { c_id: cId },
          amount: { value: amount, currency },
          ...extra,
        },
      },
    });
  }

  async getStatus({ cId, hId } = {}) {
    const identifiers = {};
    if (cId) identifiers.c_id = cId;
    if (hId) identifiers.h_id = hId;
    return this._send({
      method: 'payment.status',
      params: { payment: { identifiers } },
    });
  }
}

async function main() {
  const client = new PaymentApiClient({
    appId: process.env.PAYMENT_API_APP_ID || '1',
    apiVersion: Number(process.env.PAYMENT_API_VERSION || '2'),
    secretKey: process.env.PAYMENT_API_SECRET_KEY || 'your_secret_key',
  });

  // 1. Ping the gateway
  const pingResult = await client.ping();
  if (!pingResult.success) {
    console.error(`Gateway unreachable: ${pingResult.error.message}`);
    return;
  }
  console.log('Gateway is up');

  // 2. Check balance
  const balanceResult = await client.getBalance();
  if (balanceResult.success) {
    for (const b of balanceResult.result.balance.amounts) {
      console.log(`  ${b.currency}: ${b.value}`);
    }
  } else {
    console.error(`Balance check failed: ${balanceResult.error.message}`);
  }

  // 3. Create a deposit
  const orderId = String(Date.now());
  const depositResult = await client.createDeposit(14701, 10000, 'INR', orderId, {
    description: `Order #${orderId}`,
    payer: {
      email: 'customer@example.com',
      person: { first_name: 'John', last_name: 'Doe' },
    },
  });

  if (depositResult.success) {
    console.log(`Deposit created (request_id=${depositResult.request_id})`);
  } else {
    console.error(`Deposit failed: ${depositResult.error.message}`);
    return;
  }

  // 4. Poll for status
  for (let i = 0; i < 5; i++) {
    await new Promise((r) => setTimeout(r, 3000));
    const statusResult = await client.getStatus({ cId: orderId });
    if (statusResult.success) {
      const status = statusResult.result.payment.status.status;
      console.log(`Payment status: ${status}`);
      if (
        ['success', 'error', 'canceled', 'declined', 'refunded', 'partially_refunded'].includes(
          status,
        )
      )
        break;
    } else {
      console.error(`Status check failed: ${statusResult.error.message}`);
      break;
    }
  }
}

main().catch(console.error);
```
