# Xác thực và ký yêu cầu

> Ký các yêu cầu API bằng HMAC-SHA256 với project UUID và API key của bạn.

Mọi yêu cầu API (ngoại trừ webhook đến) đều phải kèm theo project UUID và chữ ký yêu cầu. Chữ ký chứng minh rằng yêu cầu xuất phát từ bạn và không bị ai thay đổi trên đường truyền.

## API keys

2328.io sử dụng **hai key** dùng chung một thuật toán ký nhưng áp dụng cho các endpoint khác nhau:

| Key | Dùng cho |
|-----|----------|
| **API key** | Thanh toán, ví tĩnh, số dư, tỷ giá và xác minh webhook thanh toán / ví tĩnh |
| **Payout API key** | Tất cả endpoint `/v1/payout/*` và xác minh webhook rút tiền |

Cả hai key đều nằm trong phần cài đặt project tại [2328.io](https://2328.io). Các ví dụ bên dưới ghi chung chung là "API key" — hãy thay bằng key phù hợp cho endpoint mà bạn đang gọi.

> **INFO:** **Tuyệt đối không** lẫn lộn hai key: ký một yêu cầu rút tiền bằng API key thông thường (hoặc ký yêu cầu thanh toán bằng Payout key) sẽ trả về lỗi chữ ký.

## Headers bắt buộc

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Content-Type` | string | yes | Luôn là `application/json` |
| `project` | string | yes | Project UUID của bạn |
| `sign` | string | yes | Chữ ký HMAC-SHA256 của yêu cầu, được tính bằng API key của bạn |
| `User-Agent` | string | có | Định danh ứng dụng của bạn (ví dụ `MyShop/1.4 (+https://myshop.example)`). Yêu cầu không có `User-Agent` có thể bị chặn. |

## Cách hoạt động của chữ ký

Hãy hình dung chữ ký như là dấu vân tay của thân yêu cầu. Nó được tạo ra bằng cách:

1. Serialize thân yêu cầu sang JSON (compact — không có khoảng trắng dư).
2. Mã hóa Base64 chuỗi JSON đó. Bước này chuẩn hóa đầu vào giữa các ngôn ngữ — khi đã là ASCII thuần, mọi ngôn ngữ đều cho ra cùng các byte để HMAC.
3. Tính **HMAC-SHA256** của chuỗi Base64 bằng API key của bạn, sau đó chuyển kết quả sang hex chữ thường.

Đối với **GET** và các loại yêu cầu khác không có thân, hãy ký chuỗi rỗng thay vì JSON.

> **INFO:** Chữ ký của chuỗi rỗng là cố định với một API key cho trước. Bạn có thể cache lại nếu thực hiện nhiều cuộc gọi GET.

## Triển khai

<CodeSnippet name="apiSign" langs="php,js,ts,python,go" />

### Yêu cầu không có thân (GET)

Đối với các yêu cầu không có thân (ví dụ `GET /v1/payout/status/{uuid}`), hãy ký một chuỗi rỗng:

<CodeSnippet name="apiSignBodyless" langs="curl,php,js,ts,python,go" />

## Ví dụ yêu cầu đầy đủ

<CodeSnippet name="fullRequestExample" langs="curl,php,js,ts,python,go" />

> **DANGER:** **Tuyệt đối không để lộ API key trong mã chạy phía client.** Hãy ký yêu cầu trên backend của bạn. Một API key bị rò rỉ sẽ cho bất kỳ ai toàn quyền truy cập tài khoản merchant của bạn.

## Xác minh chữ ký webhook

Khi 2328.io gửi webhook cho bạn, cùng thuật toán đó được chạy ngược lại:

1. Lấy trường `sign` ra khỏi payload.
2. Mã hóa JSON các trường còn lại (compact, không khoảng trắng).
3. Mã hóa Base64 chuỗi đó.
4. Tính `HMAC-SHA256` bằng key phù hợp.
5. So sánh với `sign` nhận được bằng cách so sánh **thời gian không đổi** (`hash_equals`, `crypto.timingSafeEqual`, `hmac.compare_digest`, `subtle.ConstantTimeCompare`, `OpenSSL.fixed_length_secure_compare`).

Key dùng để ký phụ thuộc vào nguồn webhook:

| Webhook | Key để xác minh |
|---------|---------------------|
| Webhook thanh toán / ví tĩnh (`/v1/payment`, `/v1/static-wallet`) | **API key** |
| Webhook rút tiền (`/v1/payout`) | **Payout API key** |

> **WARNING:** **Lỗi xác minh thường gặp.** Bộ mã hóa JSON của bạn phải tạo ra **đúng những byte giống hệt** như bên gửi đã tạo — nếu không, Base64 sẽ khác đi và chữ ký sẽ không khớp.

- **Go**: dùng `json.NewEncoder` với `SetEscapeHTML(false)`. `json.Marshal` mặc định sẽ escape `<`, `>`, `&` thành `<` và làm hỏng chữ ký.
- **Python**: truyền `ensure_ascii=False` vào `json.dumps`. Nếu không, các ký tự không thuộc ASCII (Kirin, Trung Quốc, …) sẽ bị escape thành `\uXXXX`.
- **JSON rút gọn**: không có khoảng trắng giữa các trường (`separators=(",", ":")` trong Python).
- **Thứ tự trường** (Go): một `map[string]any` thường sẽ ngẫu nhiên hóa thứ tự khóa khi mã hóa lại. Hãy dùng `json.RawMessage`, một struct có thứ tự, hoặc loại bỏ `sign` khỏi byte gốc.

Nếu việc xác minh vẫn thất bại, hãy tự chạy `apiSign` trên payload — nó phải tạo ra cùng chuỗi hex với `sign` đã nhận.

> **INFO:** **Một chữ ký hợp lệ không ngăn được replay.** Nó chỉ chứng minh rằng webhook đến từ 2328.io — không ngăn được kẻ tấn công gửi lại một webhook *đã bị bắt được* sau đó. Luôn kiểm tra idempotency bằng `uuid` (hoặc `txid` cho ví tĩnh) trước khi ghi có tiền. Từ chối với HTTP `401` nếu chữ ký bị thiếu hoặc sai.

Các ví dụ mã đầy đủ có trên **[Webhook Notifications](/docs/webhooks#verifying-the-signature)**. Xử lý retry và quy tắc idempotency có trong [Best practices](/docs/webhooks#best-practices).