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

# Authentication

> Authenticate LauncX V3 API requests with x-api-key, x-timestamp, and — for disbursements — the x-signature request signature.

Every API request must include your API key and a freshness proof — either a plain timestamp or a signed one:

| Header        | Value                                 | Notes                                                                                                                                                               |
| ------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-api-key`   | Your V3 API key (raw)                 | Environment-specific. Always required.                                                                                                                              |
| `x-timestamp` | Current Unix time in **milliseconds** | Must be within **±5 minutes** of server time. Required when `x-signature` is not sent; **ignored** when it is (the timestamp inside the signature is used instead). |
| `x-signature` | `t=<ms>,v1=<hex>`                     | HMAC-SHA256 request signature. **Required** on disbursement endpoints; optional elsewhere — but always verified when present.                                       |

Which endpoints require the signature:

| Endpoints                                                                  | `x-signature`                                                                |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `POST /api/v3/disbursements`, `POST /api/v3/disbursements/quote`           | **Required** — unsigned requests are rejected with `401`.                    |
| All other `/api/v3` endpoints (payments, wallets, banks, GET disbursement) | Optional. If you send it, it is verified — an invalid signature is rejected. |

<Warning>
  * Missing/invalid `x-api-key` → `401 Unauthorized`.
  * Missing/malformed `x-timestamp` (with no `x-signature`), or a value outside the ±5-minute window → `400 Bad Request` (`"Request timestamp outside valid window (5 minutes)"`).
  * Missing or invalid `x-signature` on a disbursement endpoint → `401 Unauthorized`.
  * **Holding clients** cannot use the payment/payout API → `403 Forbidden`.
</Warning>

```bash theme={null}
curl -X GET https://live.launcx.com/api/v3/payments/{id} \
  -H "x-api-key: {{your-v3-api-key}}" \
  -H "x-timestamp: {{unix_timestamp_in_milliseconds}}"
```

## Signing a request

Signing uses your **Signing Secret** — a 64-character hex string issued with your API credentials (view it in the dashboard under API credentials; regenerating credentials rotates it together with your API key and Callback Secret).

1. Serialize your request data **once** to JSON — call the result `body`. For GET requests, `body` is the empty string.
2. Take the current Unix time in milliseconds, as a string — call it `t`.
3. Build the payload to sign: `payloadToSign = t + "." + body`
4. Compute `HMAC_SHA256( payloadToSign, SigningSecret )` and encode it as **lowercase hex** — call it `sig`.
5. Send the header `x-signature: t=<t>,v1=<sig>` — and send the **same `body` string** as the HTTP body. Do not re-serialize it; the server verifies the exact bytes it receives.

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

const body = JSON.stringify({
  client_wallet_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  amount: "100000",
  bank_code: "BCA",
  account_no: "1234567890",
});
const t = Date.now().toString();
const sig = crypto
  .createHmac("sha256", process.env.SIGNING_SECRET)
  .update(`${t}.${body}`)
  .digest("hex"); // lowercase hex

await fetch("https://live.launcx.com/api/v3/disbursements", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.API_KEY,
    "Idempotency-Key": "payout-batch-42-row-7",
    "x-signature": `t=${t},v1=${sig}`,
  },
  body, // the SAME string that was signed
});
```

<Note>
  Signed requests must not use `Content-Encoding` (compression), and the body is capped at **1 MiB**. The `t` in the signature must be within ±5 minutes of server time — reuse the same `t` in the signed payload and the header.
</Note>

## Testing your signature

`POST /api/v3/debug/signature-check`

Runs the exact same verification pipeline as the enforcing endpoints, but **always returns `200`** with a field-by-field diagnostic instead of an opaque `401`. Use it to validate your signing implementation before calling business endpoints. It never takes any business action, whatever the payload.

Send any JSON body, signed exactly as you would a real request (only `x-api-key` is required; omitting `x-signature` is allowed and reports `missing_signature_header`):

```bash theme={null}
curl -X POST https://live.launcx.com/api/v3/debug/signature-check \
  -H "Content-Type: application/json" \
  -H "x-api-key: {{your-v3-api-key}}" \
  -H "x-signature: t={{t}},v1={{sig}}" \
  -d '{"hello":"world"}'
```

### Response

```json theme={null}
{
  "valid": true,
  "diagnostics": {
    "received_body": "{\"hello\":\"world\"}",
    "received_body_length": 17,
    "received_body_sha256": "93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588",
    "received_timestamp": "1719700000000",
    "server_time_ms": 1719700000123,
    "window_delta_ms": 123,
    "window_ok": true,
    "signed_payload_string": "1719700000000.{\"hello\":\"world\"}",
    "diagnosis": "ok"
  }
}
```

| `diagnosis`                      | Meaning                                                                        | Fix                                                                                                                                           |
| -------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `ok`                             | Everything matched — you're ready.                                             | —                                                                                                                                             |
| `signature_mismatch`             | Wrong secret, **or** the bytes you signed ≠ the bytes you sent.                | Compare `received_body` / `received_body_sha256` against what your code hashed — a difference means your HTTP library re-serialized the body. |
| `hex_case_mismatch`              | Your HMAC bytes are correct but `v1=` is uppercase hex.                        | Use a lowercase hex encoder.                                                                                                                  |
| `timestamp_out_of_window`        | `t` is more than 5 minutes from server time (`window_delta_ms` shows the gap). | Sync your clock via NTP, or generate `t` at send time.                                                                                        |
| `malformed_timestamp`            | `t=` is not a valid integer.                                                   | Send Unix milliseconds as decimal digits.                                                                                                     |
| `missing_signature_header`       | No `x-signature` sent.                                                         | Include the header.                                                                                                                           |
| `malformed_signature_header`     | Could not parse `t=` or `v1=`.                                                 | Check the format: `t=<ms>,v1=<64-char hex>`.                                                                                                  |
| `no_secret_provisioned`          | Your account has no Signing Secret yet.                                        | Regenerate your credentials (note: this also rotates your API key).                                                                           |
| `content_encoding_not_supported` | You sent `Content-Encoding`.                                                   | Disable compression on signed requests.                                                                                                       |
| `body_too_large`                 | Body exceeds 1 MiB.                                                            | Reduce the payload size.                                                                                                                      |

<Note>
  For security, the response never includes the server-computed signature — you hold the secret, so recompute `HMAC_SHA256( signed_payload_string, SigningSecret )` locally and compare it with your own `v1`. The endpoint is rate-limited to **20 requests/minute** per client (`429` beyond that).
</Note>
