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

# Building the X-Signature Header

> Construct the HMAC-SHA512 X-Signature header for the access token endpoint and for signed money-out API requests.

Some endpoints require a request signature for authentication, sent as the `X-Signature` header. There are **two different signature schemes**, used in two different situations. Both use **HMAC-SHA512**, but the string being signed is different — read the section relevant to the endpoint you're calling.

<Info>
  This page covers signatures you generate for **outbound** requests to the SingaPay API. To validate signatures SingaPay sends you on **inbound** webhook callbacks, see [Security and Signature Validation](/api-reference/webhooks/security-and-signature).
</Info>

| Scheme                                              | Used for                                                                                                          | Signed with          |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------- |
| [Access Token Signature](#1-access-token-signature) | Obtaining a Bearer access token (`POST /api/v1.1/access-token/b2b`)                                               | Your `client_secret` |
| [Request Signature](#2-request-signature)           | Authenticating individual requests to money-out endpoints (Disbursement, Account Transfer, E-Wallet Top Up, etc.) | Your `client_secret` |

***

## 1. Access Token Signature

Used only when calling `POST /api/v1.1/access-token/b2b` to obtain a Bearer token.

### Formula

```text theme={null}
payload     = "{client_id}_{client_secret}_{YYYYMMDD}"
X-Signature = HMAC-SHA512(payload, client_secret)   // lowercase hex digest
```

<Note>
  `YYYYMMDD` is the **current server date** (UTC+7 / Asia/Jakarta), e.g. `20260727`. The signature is only valid for that calendar day.
</Note>

### Required headers

| Header         | Description                 |
| -------------- | --------------------------- |
| `X-PARTNER-ID` | Your merchant API key       |
| `X-CLIENT-ID`  | Your `client_id`            |
| `X-Signature`  | Signature computed as above |

### Request body

```json theme={null}
{
  "grant_type": "client_credentials"
}
```

### Examples

<CodeGroup>
  ```php PHP theme={null}
  $clientId     = 'your_client_id';
  $clientSecret = 'your_client_secret';
  $currentDate  = date('Ymd'); // e.g. 20260727

  $payload   = "{$clientId}_{$clientSecret}_{$currentDate}";
  $signature = hash_hmac('sha512', $payload, $clientSecret);

  // Headers:
  // X-PARTNER-ID: your_api_key
  // X-CLIENT-ID: your_client_id
  // X-Signature: <$signature>
  ```

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

  const clientId = 'your_client_id';
  const clientSecret = 'your_client_secret';
  const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD (Asia/Jakarta date)

  const payload = `${clientId}_${clientSecret}_${currentDate}`;
  const signature = crypto.createHmac('sha512', clientSecret).update(payload).digest('hex');
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  from datetime import datetime

  client_id = 'your_client_id'
  client_secret = 'your_client_secret'
  current_date = datetime.now().strftime('%Y%m%d')  # Asia/Jakarta date

  payload = f'{client_id}_{client_secret}_{current_date}'
  signature = hmac.new(client_secret.encode(), payload.encode(), hashlib.sha512).hexdigest()
  ```
</CodeGroup>

Use the returned `access_token` as `Authorization: Bearer <access_token>` on subsequent requests.

***

## 2. Request Signature

Used for money-out endpoints that require per-request signing, e.g.:

* `POST /api/v2.0/disbursement/transfer`
* `POST /api/v2.0/ewallet/trigger-topup`
* Account Transfer endpoints

<Steps>
  <Step title="Normalize the request body">
    Sort all object keys **recursively and alphabetically**.
  </Step>

  <Step title="Hash the normalized body">
    Hash the normalized JSON with **SHA-256** to get `hashed_body` (hex digest).
  </Step>

  <Step title="Build the string to sign">
    ```text theme={null}
    string_to_sign = "{METHOD}:{ENDPOINT}:{ACCESS_TOKEN}:{hashed_body}:{TIMESTAMP}"
    ```
  </Step>

  <Step title="Sign it">
    ```text theme={null}
    X-Signature = HMAC-SHA512(string_to_sign, client_secret)   // lowercase hex digest
    ```
  </Step>
</Steps>

Where:

* `METHOD` — HTTP method in uppercase, e.g. `POST`.
* `ENDPOINT` — the request path **including query string**, e.g. `/api/v2.0/disbursement/transfer`. Do not include the domain.
* `ACCESS_TOKEN` — the Bearer token obtained from the access-token endpoint (without the `Bearer ` prefix).
* `TIMESTAMP` — current Unix timestamp in **seconds** (not milliseconds), sent as a string.

### Required headers

| Header          | Description                                    |
| --------------- | ---------------------------------------------- |
| `X-PARTNER-ID`  | Your merchant API key                          |
| `Authorization` | `Bearer <access_token>`                        |
| `X-Timestamp`   | Unix timestamp (seconds) used in the signature |
| `X-Signature`   | Signature computed as above                    |

<Warning>
  Regenerate the signature for **every request** — the timestamp and body are always part of the signed string, so a signature cannot be reused across requests or replayed after it expires.
</Warning>

### Examples

<CodeGroup>
  ```php PHP theme={null}
  $method      = 'POST';
  $endpoint    = '/api/v2.0/disbursement/transfer'; // path + query string, no domain
  $accessToken = 'your_bearer_token';
  $secretKey   = 'your_client_secret';
  $timestamp   = (string) time();

  // 1. Normalize body (recursive key sort)
  function sortRecursive(array &$arr): void {
      ksort($arr, SORT_STRING);
      foreach ($arr as &$value) {
          if (is_array($value)) {
              sortRecursive($value);
          }
      }
  }

  $body = [
      'account_number' => '1234567890',
      'amount' => 100000,
      'bank_code' => 'BCA',
  ];
  sortRecursive($body);
  $normalizedJson = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

  // 2. Hash body
  $hashedBody = hash('sha256', $normalizedJson);

  // 3. Build string to sign
  $stringToSign = "{$method}:{$endpoint}:{$accessToken}:{$hashedBody}:{$timestamp}";

  // 4. Sign
  $signature = hash_hmac('sha512', $stringToSign, $secretKey);

  // Headers:
  // X-PARTNER-ID: your_api_key
  // Authorization: Bearer your_bearer_token
  // X-Timestamp: <$timestamp>
  // X-Signature: <$signature>
  ```

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

  function sortKeysRecursive(obj) {
    if (Array.isArray(obj)) return obj.map(sortKeysRecursive);
    if (obj !== null && typeof obj === 'object') {
      return Object.keys(obj).sort().reduce((acc, key) => {
        acc[key] = sortKeysRecursive(obj[key]);
        return acc;
      }, {});
    }
    return obj;
  }

  const method = 'POST';
  const endpoint = '/api/v2.0/disbursement/transfer';
  const accessToken = 'your_bearer_token';
  const secretKey = 'your_client_secret';
  const timestamp = Math.floor(Date.now() / 1000).toString();

  const body = { account_number: '1234567890', amount: 100000, bank_code: 'BCA' };
  const normalizedJson = JSON.stringify(sortKeysRecursive(body));

  const hashedBody = crypto.createHash('sha256').update(normalizedJson).digest('hex');
  const stringToSign = `${method}:${endpoint}:${accessToken}:${hashedBody}:${timestamp}`;
  const signature = crypto.createHmac('sha512', secretKey).update(stringToSign).digest('hex');
  ```

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

  def sort_keys_recursive(obj):
      if isinstance(obj, list):
          return [sort_keys_recursive(v) for v in obj]
      if isinstance(obj, dict):
          return {k: sort_keys_recursive(obj[k]) for k in sorted(obj.keys())}
      return obj

  method = 'POST'
  endpoint = '/api/v2.0/disbursement/transfer'
  access_token = 'your_bearer_token'
  secret_key = 'your_client_secret'
  timestamp = str(int(time.time()))

  body = {'account_number': '1234567890', 'amount': 100000, 'bank_code': 'BCA'}
  normalized_json = json.dumps(sort_keys_recursive(body), separators=(',', ':'), ensure_ascii=False)

  hashed_body = hashlib.sha256(normalized_json.encode()).hexdigest()
  string_to_sign = f'{method}:{endpoint}:{access_token}:{hashed_body}:{timestamp}'
  signature = hmac.new(secret_key.encode(), string_to_sign.encode(), hashlib.sha512).hexdigest()
  ```
</CodeGroup>

***

## Common mistakes

| Symptom                                    | Likely cause                                                                                                                                    |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Signature always invalid                   | `ENDPOINT` doesn't match the exact request path (including query string) sent to the server                                                     |
| Signature valid on retry but not first try | Body was modified/reformatted between signing and sending (e.g. number vs. string, key order, extra whitespace)                                 |
| Signature works for GET but not POST/PUT   | `METHOD` not uppercase, or body normalization skipped for empty body                                                                            |
| Intermittent 401                           | Timestamp too old — requests are only accepted within a limited time window from `X-Timestamp`; generate a fresh signature right before sending |
| Works locally, fails in production         | Client secret or client ID mismatch, or numeric fields serialized differently (e.g. `100000` vs `"100000.00"`) across environments              |

<AccordionGroup>
  <Accordion title="Best practices" icon="circle-check" iconType="solid">
    * Never log or expose your `client_secret` in client-side code, logs, or version control.
    * Always use HTTPS.
    * Generate the signature immediately before sending the request — don't reuse an old signature/timestamp pair.
  </Accordion>
</AccordionGroup>
