> ## 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.

# Authentication

> Learn how to authenticate your API requests with 123hub

# Authentication

Every client is configured with one explicit **Application ID + API version**
pair. Merchant API v1 keeps the legacy exact-body SHA512 contract. Merchant API
v2 uses a domain-separated HMAC-SHA512 contract with mandatory timestamp/nonce
replay protection. The server never chooses a default or falls back between
versions.

## Obtaining Credentials

<Info>
  API credentials and merchant accounts are created by the 123hub team during
  onboarding. Self-registration is not available.
</Info>

To obtain your credentials:

1. Contact your 123hub account manager
2. Or email [support@123hub.pro](mailto:support@123hub.pro)

Once your account is set up, you can view your credentials in the [Merchant Dashboard](https://admin.bafanglaicai88.com) under **Settings > API Keys**.

You will receive:

| Credential       | Type    | Description                                          |
| ---------------- | ------- | ---------------------------------------------------- |
| `application_id` | Integer | Your unique application identifier (e.g., `1`, `42`) |
| `secret_key`     | String  | A random secret string used to sign requests         |

<Warning>
  Keep your `secret_key` secure. Never expose it in client-side code, public
  repositories, or browser requests. If compromised, rotate it immediately from
  the dashboard.
</Warning>

## Authentication Headers

Every request to `POST /public/api/multihub/v1` must include the first two
headers below. For that endpoint the replay headers are optional, but when used
they must be sent together:

| Header                  | Value        | Description                              |
| ----------------------- | ------------ | ---------------------------------------- |
| `X-Data-Application-Id` | Integer      | Your application ID                      |
| `X-Data-Hash`           | Hex string   | SHA512 hash of `requestBody + secretKey` |
| `X-Data-Timestamp`      | Unix seconds | Optional current request timestamp       |
| `X-Data-Nonce`          | String       | Optional unique nonce for this request   |

For `POST /public/api/multihub/v2`, send all four headers below:

| Header                  | Value        | Description                                    |
| ----------------------- | ------------ | ---------------------------------------------- |
| `X-Data-Application-Id` | Integer      | Exact appId selected with API version 2        |
| `X-Data-Timestamp`      | Unix seconds | Canonical seconds within ±300 seconds          |
| `X-Data-Nonce`          | String       | Unique 16–128 character nonce                  |
| `X-Data-Signature`      | Hex string   | Lowercase HMAC-SHA512 over the canonical input |

The v2 request canonical payload has no trailing newline:

```text theme={null}
quadpay-multihub-request-v2
{applicationId}
POST
/public/api/multihub/v2
{unixTimestamp}
{nonce}
{sha256(rawBody)}
```

Compute `X-Data-Signature = HMAC-SHA512(secretKey, canonicalPayload)`. The path
is the fixed literal shown above. Redis replay storage is mandatory for v2; the
request fails closed if the nonce cannot be reserved. Do not retry v2 as v1.

Successful v2 responses use `X-Data-Signature` over:

```text theme={null}
quadpay-multihub-response-v2
{applicationId}
{requestNonce}
{requestId}
{httpStatus}
{sha256(rawResponseBody)}
```

## How Signing Works

<Steps>
  <Step title="Prepare the request body">
    Serialize your request body as a JSON string. This is the exact string that
    will be sent as the HTTP body.
  </Step>

  <Step title="Compute the hash">
    Concatenate the JSON string with your `secret_key` (no separator), then
    compute the SHA512 hash of the result. Output as a lowercase hex string. `    hash = SHA512(jsonBody + secretKey)`
  </Step>

  <Step title="Send the request">
    Send the two required headers. You may also include timestamp and nonce as a
    pair, as shown below. Webhook resend also accepts the two-header legacy
    mode; when you send both replay headers, the route-bound signing rules in
    [Webhooks](/api-reference/webhooks) apply.
  </Step>
</Steps>

<Warning>
  The JSON string you hash **must be byte-for-byte identical** to the JSON
  string sent as the HTTP body. If you use compact serialization (e.g.,
  `json.dumps(body, separators=(',',':'))` in Python), you must send that exact
  compact string as the request body. Differences in whitespace, key ordering,
  or formatting between the hashed string and the sent body will cause error
  `3000` (Authentication error).
</Warning>

## Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  # Step 1: Define your body
  BODY='{"method":"gateway.ping","params":{}}'

  # Step 2: Compute the SHA512 hash and replay proof
  # Concatenate body + secret key, then hash
  HASH=$(printf '%s%s' "${BODY}" "your_secret_key" | sha512sum | awk '{print $1}')
  TIMESTAMP=$(date +%s)
  NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')

  # Step 3: Send the request
  curl -X POST https://api.bafanglaicai88.com/public/api/multihub/v1 \
    -H "Content-Type: application/json" \
    -H "X-Data-Application-Id: 1" \
    -H "X-Data-Hash: ${HASH}" \
    -H "X-Data-Timestamp: ${TIMESTAMP}" \
    -H "X-Data-Nonce: ${NONCE}" \
    --data-binary "${BODY}"
  ```

  ```python Python theme={null}
  import hashlib
  import json
  import time
  import uuid
  import requests

  APP_ID = "1"
  SECRET_KEY = "your_secret_key"
  BASE_URL = "https://api.bafanglaicai88.com/public/api/multihub/v1"

  # Step 1: Prepare the request body
  body = {"method": "gateway.ping", "params": {}}
  body_str = json.dumps(body, separators=(',', ':'))

  # Step 2: Compute the SHA512 hash
  hash_value = hashlib.sha512((body_str + SECRET_KEY).encode()).hexdigest()

  # Step 3: Send the request
  response = requests.post(BASE_URL, data=body_str, headers={
      "Content-Type": "application/json",
      "X-Data-Application-Id": APP_ID,
      "X-Data-Hash": hash_value,
      "X-Data-Timestamp": str(int(time.time())),
      "X-Data-Nonce": str(uuid.uuid4()),
  }, timeout=(3.05, 30))

  print(response.json())
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  const APP_ID = "1";
  const SECRET_KEY = "your_secret_key";
  const BASE_URL = "https://api.bafanglaicai88.com/public/api/multihub/v1";

  // Step 1: Prepare the request body
  const body = { method: "gateway.ping", params: {} };
  const bodyStr = JSON.stringify(body);

  // Step 2: Compute the SHA512 hash
  const hash = crypto
    .createHash("sha512")
    .update(bodyStr + SECRET_KEY)
    .digest("hex");
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomUUID();

  // Step 3: Send the request
  const response = await fetch(BASE_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Data-Application-Id": APP_ID,
      "X-Data-Hash": hash,
      "X-Data-Timestamp": timestamp,
      "X-Data-Nonce": nonce,
    },
    body: bodyStr,
    signal: AbortSignal.timeout(30000),
  });

  const data = await response.json();
  console.log(data);
  ```

  ```php PHP theme={null}
  <?php
  $appId = '1';
  $secretKey = 'your_secret_key';
  $baseUrl = 'https://api.bafanglaicai88.com/public/api/multihub/v1';

  // Step 1: Prepare the request body
  $body = json_encode([
      'method' => 'gateway.ping',
      'params' => new stdClass(),
  ]);

  // Step 2: Compute the SHA512 hash
  $hash = hash('sha512', $body . $secretKey);
  $timestamp = (string) time();
  $nonce = bin2hex(random_bytes(16));

  // Step 3: Send the request
  $ch = curl_init($baseUrl);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'Content-Type: application/json',
      "X-Data-Application-Id: $appId",
      "X-Data-Hash: $hash",
      "X-Data-Timestamp: $timestamp",
      "X-Data-Nonce: $nonce",
  ]);
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
  curl_setopt($ch, CURLOPT_TIMEOUT, 30);

  $response = curl_exec($ch);
  curl_close($ch);

  echo $response;
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"crypto/sha512"
  	"encoding/hex"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"time"
  )

  func main() {
  	appID := "1"
  	secretKey := "your_secret_key"
  	baseURL := "https://api.bafanglaicai88.com/public/api/multihub/v1"

  	// Step 1: Prepare the request body
  	body := map[string]interface{}{
  		"method": "gateway.ping",
  		"params": map[string]interface{}{},
  	}
  	bodyBytes, _ := json.Marshal(body)

  	// Step 2: Compute the SHA512 hash
  	hasher := sha512.New()
  	hasher.Write(bodyBytes)
  	hasher.Write([]byte(secretKey))
  	hashHex := hex.EncodeToString(hasher.Sum(nil))
  	timestamp := fmt.Sprintf("%d", time.Now().Unix())
  	nonce := fmt.Sprintf("%d", time.Now().UnixNano())

  	// Step 3: Send the request
  	req, _ := http.NewRequest("POST", baseURL, bytes.NewBuffer(bodyBytes))
  	req.Header.Set("Content-Type", "application/json")
  	req.Header.Set("X-Data-Application-Id", appID)
  	req.Header.Set("X-Data-Hash", hashHex)
  	req.Header.Set("X-Data-Timestamp", timestamp)
  	req.Header.Set("X-Data-Nonce", nonce)

  	client := &http.Client{Timeout: 30 * time.Second}
  	resp, _ := client.Do(req)
  	defer resp.Body.Close()

  	respBody, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(respBody))
  }
  ```
</CodeGroup>

## Verifying API Response Signatures

Successful API responses include `X-Data-Hash`. Verify it with the same merchant API secret:

```
SHA512(rawResponseBody + apiSecret)
```

Use the raw response body bytes exactly as received. This check protects your integration from tampered responses and should be performed before trusting payment identifiers, balances, or status values.

## Verifying Webhook Signatures

When 123hub sends webhook notifications to your server, the request includes an `X-Data-Hash` header. New deliveries also include replay-protection metadata in `X-Webhook-Id`, `X-Webhook-Timestamp`, `X-Webhook-Nonce`, `X-Webhook-Signature-V2`, and `X-Webhook-Signature-V3`. Prefer `X-Webhook-Signature-V3` when present, and fall back to V2 or `X-Data-Hash` only for legacy deliveries.

`X-Webhook-Signature-V3` is `HMAC-SHA512(timestamp.nonce.webhookId.rawBody, webhook secret)`. `X-Data-Hash` remains `SHA512(webhookBody + webhook secret)`.

Webhook create/rotation reveals a dedicated webhook-only secret once. Store it
separately from the API credential; rotating one does not rotate the other.
Historical API-compatible webhook rows are blocked until explicit webhook secret
rotation. For a callback auto-created through `params.payment.webhook_url`, derive
the lowercase webhook secret with
`HMAC-SHA256(apiSecret, "quadpay:merchant-webhook-signing:v1")`, using the exact
API secret that authenticated that payment create. Test and production keys do
not cross; the resulting callback destination is scoped to that payment.

<CodeGroup>
  ```python Python — Verify Webhook theme={null}
  import hashlib
  import hmac

  def verify_webhook(raw_body: bytes, signature: str, webhook_secret: str) -> bool:
      expected = hashlib.sha512(raw_body + webhook_secret.encode()).hexdigest()
      return hmac.compare_digest(expected, signature)  # constant-time comparison
  ```

  ```javascript Node.js — Verify Webhook theme={null}
  const crypto = require("crypto");

  function verifyWebhook(rawBody, signature, webhookSecret) {
    const expected = crypto
      .createHash("sha512")
      .update(Buffer.concat([Buffer.from(rawBody), Buffer.from(webhookSecret)]))
      .digest("hex");
    if (!/^[a-f0-9]{128}$/i.test(signature || "")) {
      return false;
    }
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(signature, "hex"),
    );
  }

  function verifyWebhookV2(rawBody, signature, timestamp, webhookSecret) {
    const timestampMs = Date.parse(timestamp || "");
    if (
      !Number.isFinite(timestampMs) ||
      Math.abs(Date.now() - timestampMs) > 5 * 60 * 1000
    ) {
      return false;
    }
    const expected = crypto
      .createHash("sha512")
      .update(
        String(timestamp) + Buffer.from(rawBody).toString("utf8") + webhookSecret,
      )
      .digest("hex");
    if (!/^[a-f0-9]{128}$/i.test(signature || "")) {
      return false;
    }
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(signature, "hex"),
    );
  }

  function verifyWebhookV3(
    rawBody,
    signature,
    timestamp,
    nonce,
    webhookId,
    webhookSecret,
  ) {
    const timestampMs = Date.parse(timestamp || "");
    if (!timestamp || !nonce || !webhookId || !Number.isFinite(timestampMs)) {
      return false;
    }
    if (Math.abs(Date.now() - timestampMs) > 5 * 60 * 1000) {
      return false;
    }
    const expected = crypto
      .createHmac("sha512", webhookSecret)
      .update(
        [timestamp, nonce, webhookId, Buffer.from(rawBody).toString("utf8")].join(
          ".",
        ),
      )
      .digest("hex");
    if (!/^[a-f0-9]{128}$/i.test(signature || "")) {
      return false;
    }
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(signature, "hex"),
    );
  }
  ```

  ```php PHP — Verify Webhook theme={null}
  <?php
  function verifyWebhook(string $rawBody, string $signature, string $webhookSecret): bool {
      $expected = hash('sha512', $rawBody . $webhookSecret);
      return hash_equals($expected, $signature);
  }
  ```

  ```go Go — Verify Webhook theme={null}
  import (
  	"crypto/sha512"
  	"crypto/subtle"
  	"encoding/hex"
  )

  func verifyWebhook(rawBody []byte, signature string, webhookSecret string) bool {
  	hasher := sha512.New()
  	hasher.Write(rawBody)
  	hasher.Write([]byte(webhookSecret))
  	expected := hex.EncodeToString(hasher.Sum(nil))
  	return subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) == 1
  }
  ```
</CodeGroup>

<Warning>
  Always verify webhook signatures before processing the payload. Use a
  constant-time comparison function (like `hash_equals` in PHP or
  `crypto.timingSafeEqual` in Node.js) to prevent timing attacks.
</Warning>

## Test Mode vs Production

<Info>
  Test and production requests use the **same API endpoint** and the same
  authentication mechanism. The environment is determined by which credentials
  you use.
</Info>

* **Test credentials** identify an account-specific test environment and routes confirmed during onboarding
* **Production credentials** create real transactions with actual money movement
* Credentials whose environment does not match the merchant are rejected. Test
  payments select sandbox provider credentials only and never fall back to a
  production provider credential or endpoint.
* Webhooks are delivered in both environments for testing integrations
* All API responses follow the same format in both environments

## Rate Limits

API requests may be rate-limited to ensure fair usage. The public public API controller is exempt from the gateway default IP throttler, but authenticated applications have protective per-method buckets for `payment.in`, `payment.out`, `payment.status`, and `balance.get`.

| Scope                  | Limit                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| 123hub API             | Per merchant/application/method bucket; production limits are confirmed during onboarding |
| Webhook management API | Subject to gateway/API-key policy                                                         |

When a rate limit is applied, it is counted per authenticated merchant/application context and method.

<Note>
  If you exceed an applied limit, the response returns HTTP `429` with the
  standard API error envelope and `Retry-After-*` headers. Retry with backoff or
  contact support if you need a higher production throughput profile.
</Note>

## Error Responses

Authentication errors are returned in the standard response envelope with HTTP 400. Credential policy or merchant-ownership denials return HTTP `403` with code `3008`. An enforced credential daily-request limit returns HTTP `429` with code `3009`; its counter resets at UTC day rollover. DTO validation failures, including unknown fields rejected by strict validation, return code `9000` when request validation reaches the DTO layer. During deployments, readiness gating can return a non-envelope HTTP `503` before API handling starts.

```json Invalid Application ID theme={null}
{
  "success": false,
  "error": {
    "code": 3003,
    "message": "The app does not exist",
    "details": null,
    "context": null
  },
  "request_id": "req_a1b2c3d4",
  "processing_time": 2
}
```

```json Invalid Hash Signature theme={null}
{
  "success": false,
  "error": {
    "code": 3000,
    "message": "Authentication error",
    "details": null,
    "context": null
  },
  "request_id": "req_e5f6g7h8",
  "processing_time": 1
}
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Use Environment Variables" icon="lock">
    Store your `application_id` and `secret_key` in environment variables, never
    in source code
  </Card>

  <Card title="Rotate Keys Regularly" icon="arrows-rotate">
    Regenerate your secret key periodically from the dashboard for enhanced
    security
  </Card>

  <Card title="Verify Webhooks" icon="shield">
    Always verify the `X-Data-Hash` header on incoming webhooks before
    processing
  </Card>

  <Card title="Monitor Usage" icon="chart-line">
    Track API usage in the dashboard to detect anomalies and stay within rate
    limits
  </Card>
</CardGroup>
