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

# Authentication

> 2 tier xác thực: API key đơn giản và HMAC signing đầy đủ.

API chia 2 tier theo mức độ nhạy cảm của dữ liệu. Tier 1 là mặc định
cho dữ liệu thị trường công khai; Tier 2 áp dụng cho các endpoint
user-scoped (đánh dấu riêng trong API Reference).

## Tier 1 — Chỉ cần API key

Chỉ cần gửi header `X-FH-APIKEY` trên mọi request. Áp dụng cho các
endpoint dữ liệu thị trường công khai:

* Toàn bộ `GET /market/**`
* `GET /trading/market/**`
* `GET /trading/securities/**`
* Toàn bộ `GET /fund-trading/public/**` (quỹ mở)

```bash theme={null}
curl -H "X-FH-APIKEY: $FINHAY_API_KEY" \
  "https://open-api.fhsc.com.vn/market/stock-realtime?symbol=VNM"
```

## Tier 2 — HMAC signing đầy đủ

Áp dụng cho các endpoint liên quan đến thông tin user / tài khoản / lệnh
giao dịch / lãi-lỗ. Mỗi request cần đủ 4 header (cộng `X-FH-BODYHASH`
khi request có body):

| Header           | Khi nào               | Giá trị                                        |
| ---------------- | --------------------- | ---------------------------------------------- |
| `X-FH-APIKEY`    | Luôn luôn             | Client API key (dài hạn)                       |
| `X-FH-TIMESTAMP` | Tier 2                | Unix time hiện tại, đơn vị **mili-giây**       |
| `X-FH-NONCE`     | Tier 2                | UUIDv4, duy nhất per request                   |
| `X-FH-SIGNATURE` | Tier 2                | `HMAC_SHA256(secret, payload)` — hex lowercase |
| `X-FH-BODYHASH`  | Tier 2 **và** có body | `SHA256(body)` — hex lowercase                 |

Endpoint Tier 2 hiện có:

* `GET /users/v1/users/me`
* `GET /users/v1/users/{userId}/sub-accounts`
* `GET /users/v3/users/{userId}/assets/summary`
* `GET /trading/accounts/{subAccountId}/summary`
* `GET /trading/v1/accounts/{subAccountId}/order-book`
* `GET /trading/v1/accounts/{subAccountId}/order-book/{orderId}`
* `GET /trading/v2/sub-accounts/{subAccountId}/portfolio`
* `GET /trading/v5/account/{subAccountId}/user-rights`
* `GET /trading/pnl-today/{userId}`

Endpoint Tier 2 thuộc nhóm **Thực thi lệnh** (preview) — yêu cầu thêm
`X-FH-BODYHASH` (có body) và `X-FH-2FA-TOKEN` (daily 2FA session):

* `POST /trading/oa/sub-accounts/{subAccountId}/orders`
* `PUT /trading/oa/sub-accounts/{subAccountId}/orders/{orderId}`
* `DELETE /trading/oa/sub-accounts/{subAccountId}/orders/{orderId}`

<Warning>
  Endpoint nhóm **Thực thi lệnh** đang ở giai đoạn **preview**.
</Warning>

## Signing payload

Payload đầu vào cho HMAC được build theo template dưới (mỗi field nối
bằng `\n`):

```
{TIMESTAMP}\n{METHOD}\n{PATH}[?{QUERY}]\n{BODYHASH}
```

<Steps>
  <Step title="TIMESTAMP">
    Unix mili-giây, chuỗi số thập phân (ví dụ `"1714464000123"`). Server
    chấp nhận lệch tối đa **±30 giây** so với giờ thực — đảm bảo client
    đồng bộ NTP.
  </Step>

  <Step title="METHOD">
    HTTP method viết hoa: `GET`, `POST`, `PUT`, `DELETE`.
  </Step>

  <Step title="PATH [?QUERY]">
    Path của request, ví dụ `/trading/accounts/0001234567/summary`. Nếu
    request có query string, nối thêm với prefix `?` (ví dụ
    `/market/stock-realtime?symbol=VNM`). **Bỏ hoàn toàn** segment `?`
    nếu không có query.
  </Step>

  <Step title="BODYHASH">
    `SHA256(body)` ở dạng hex lowercase. Là **chuỗi rỗng** khi body rỗng.
    Nối thẳng sau `\n` cuối cùng — **không** thêm newline riêng sau
    `BODYHASH`.
  </Step>
</Steps>

<Warning>
  **Replay protection**: server nhớ cặp `(apiKey, nonce)` trong **5 phút**.
  Nonce reused trong window đó sẽ bị từ chối với mã `AUTH_NONCE_REUSED`
  (HTTP 401). Mỗi request phải sinh nonce mới — recommend UUIDv4.
</Warning>

Toàn bộ metadata signing (algorithm, tên header, replay window, …) được
expose qua OpenAPI extension `x-finhay-signing` ở root spec, để các tool
codegen tự sinh signing layer.

## 2FA session (`X-FH-2FA-TOKEN`)

Riêng các write operation thuộc nhóm **Thực thi lệnh** (preview — `POST`
/ `PUT` / `DELETE` dưới `/trading/oa/**`), ngoài 5 header HMAC client
còn phải gửi kèm `X-FH-2FA-TOKEN` — daily JWT session token do server
cấp sau khi user xác thực qua OTP.

* Token có tuổi thọ **1 ngày giao dịch**, scope theo `apiKey`.
* Lưu vào memory hoặc secret store; tự re-init khi sang ngày mới.
* Khi token hết hạn / không hợp lệ / bị revoke, server trả `HTTP 403`
  với `error_code` là 1 trong:
  * `OTP_SESSION_REQUIRED`
  * `OTP_SESSION_EXPIRED`
  * `OTP_SESSION_INVALID`
  * `OTP_SESSION_REVOKED`
* Bắt 1 trong 4 mã trên → chạy lại OTP flow để lấy token mới → retry
  request.

<Note>
  OTP flow để xin `X-FH-2FA-TOKEN` không thuộc surface của tài liệu này.
  Liên hệ team Finhay để biết chi tiết quy trình nội bộ.
</Note>

## Signing middleware

<Warning>
  OpenAPI generator thường sinh 5 setter độc lập (`setApiKey`,
  `setTimestamp`, `setNonce`, `setSignature`, `setBodyHash`) — **chỉ API
  key được set bằng tay**. 4 setter còn lại cần được tính per-request
  bởi middleware chạy ngay trước HTTP call. Tích hợp đoạn code dưới vào
  client (hoặc tương đương) và bỏ qua các setter còn lại.
</Warning>

<CodeGroup>
  ```ts TypeScript (axios) theme={null}
  import axios from "axios";
  import crypto from "node:crypto";

  const API_KEY    = process.env.FINHAY_API_KEY!;
  const API_SECRET = process.env.FINHAY_API_SECRET!;
  const BASE_URL   = process.env.FINHAY_BASE_URL ?? "https://open-api.fhsc.com.vn";

  const API_KEY_ONLY: RegExp[] = [
    /^\/market\/.*$/,
    /^\/trading\/market\/.*$/,
    /^\/trading\/securities\/.*$/,
    /^\/fund-trading\/public\/.*$/,
  ];

  const isApiKeyOnly = (method: string, path: string): boolean =>
    method === "GET" && API_KEY_ONLY.some((re) => re.test(path));

  export const client = axios.create({ baseURL: BASE_URL });

  client.interceptors.request.use((config) => {
    const method = (config.method ?? "get").toUpperCase();
    const url    = new URL(config.url ?? "", BASE_URL);
    const path   = url.pathname;

    // Luôn gửi API key.
    config.headers.set("X-FH-APIKEY", API_KEY);

    // Tier 1: market read — dừng ở đây.
    if (isApiKeyOnly(method, path)) return config;

    // Tier 2: HMAC sign request.
    const timestamp = Date.now().toString();
    const nonce     = crypto.randomUUID();
    const query     = url.search.slice(1); // bỏ leading '?'
    const body      = typeof config.data === "string"
      ? config.data
      : config.data != null
        ? JSON.stringify(config.data)
        : "";
    const bodyHash  = body
      ? crypto.createHash("sha256").update(body).digest("hex")
      : "";

    let payload = `${timestamp}\n${method}\n${path}`;
    if (query)    payload += `?${query}`;
    payload += "\n";
    if (bodyHash) payload += bodyHash;

    const signature = crypto
      .createHmac("sha256", API_SECRET)
      .update(payload)
      .digest("hex");

    config.headers.set("X-FH-TIMESTAMP", timestamp);
    config.headers.set("X-FH-NONCE",     nonce);
    config.headers.set("X-FH-SIGNATURE", signature);
    if (bodyHash) config.headers.set("X-FH-BODYHASH", bodyHash);

    // Preview: write ops dưới /trading/oa/** cần thêm 2FA token.
    if (/^\/trading\/oa\//.test(path)) {
      const token = process.env.FINHAY_2FA_TOKEN;
      if (!token) throw new Error("Missing FINHAY_2FA_TOKEN — run OTP flow first");
      config.headers.set("X-FH-2FA-TOKEN", token);
    }

    return config;
  });
  ```

  ```python Python (requests) theme={null}
  import hashlib
  import hmac
  import json
  import os
  import re
  import time
  import uuid
  from urllib.parse import urlsplit

  import requests

  API_KEY    = os.environ["FINHAY_API_KEY"]
  API_SECRET = os.environ["FINHAY_API_SECRET"].encode()
  BASE_URL   = os.environ.get("FINHAY_BASE_URL", "https://open-api.fhsc.com.vn")

  API_KEY_ONLY = [
      re.compile(r"^/market/.*$"),
      re.compile(r"^/trading/market/.*$"),
      re.compile(r"^/trading/securities/.*$"),
      re.compile(r"^/fund-trading/public/.*$"),
  ]

  def is_api_key_only(method: str, path: str) -> bool:
      return method == "GET" and any(p.match(path) for p in API_KEY_ONLY)

  def signed_request(method: str, path: str, *, params=None, data=None):
      method = method.upper()
      parts  = urlsplit(BASE_URL + path)
      headers = {"X-FH-APIKEY": API_KEY}

      if is_api_key_only(method, parts.path):
          return requests.request(method, BASE_URL + path, params=params, headers=headers)

      body = ""
      if data is not None:
          body = data if isinstance(data, str) else json.dumps(data, separators=(",", ":"))
      body_hash = hashlib.sha256(body.encode()).hexdigest() if body else ""

      # Build canonical query string from params (preserve insertion order).
      query = "&".join(f"{k}={v}" for k, v in (params or {}).items())

      timestamp = str(int(time.time() * 1000))
      nonce     = str(uuid.uuid4())

      payload = f"{timestamp}\n{method}\n{parts.path}"
      if query:     payload += f"?{query}"
      payload += "\n"
      if body_hash: payload += body_hash

      signature = hmac.new(API_SECRET, payload.encode(), hashlib.sha256).hexdigest()

      headers.update({
          "X-FH-TIMESTAMP": timestamp,
          "X-FH-NONCE":     nonce,
          "X-FH-SIGNATURE": signature,
      })
      if body_hash:
          headers["X-FH-BODYHASH"] = body_hash
          headers.setdefault("Content-Type", "application/json")

      # Preview: write ops dưới /trading/oa/** cần thêm 2FA token.
      if parts.path.startswith("/trading/oa/"):
          token = os.environ.get("FINHAY_2FA_TOKEN")
          if not token:
              raise RuntimeError("Missing FINHAY_2FA_TOKEN — run OTP flow first")
          headers["X-FH-2FA-TOKEN"] = token

      return requests.request(
          method, BASE_URL + path,
          params=params, data=body or None, headers=headers,
      )
  ```
</CodeGroup>

<Note>
  **Lưu ý về form "Try It"**: form gọi thử endpoint tích hợp trong tab
  API Reference KHÔNG tự tính HMAC signature. Để test endpoint Tier 2,
  hãy dùng Postman (setup pre-request script để sign), hoặc tự tính
  signature rồi paste vào header tương ứng.
</Note>
