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

# Xác thực

> API Key cho Integration API, lấy access token cho SDK/iframe qua partner-login, và vòng đời token

econtractid có **hai loại thông tin xác thực** cho hai mục đích khác nhau:

| Loại             | Dùng khi                                                                           | Cơ chế                       |
| ---------------- | ---------------------------------------------------------------------------------- | ---------------------------- |
| **API Key**      | Backend của bạn gọi Integration API (tạo hồ sơ, tra cứu, tải file)                 | Header `X-API-Key`           |
| **Access Token** | Người dùng cuối thao tác trên giao diện econtractid (nhúng iframe hoặc SDK mobile) | JWT, lấy qua `partner-login` |

API Key lại có **hai loại**, cấp cho hai việc khác nhau — dùng nhầm sẽ bị từ chối:

| Loại API Key     | Dùng cho                                 | Ràng buộc                                                                                                 |
| ---------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Theo tổ chức** | Integration API (`/integration/*`)       | Gắn sẵn tổ chức, và **phải gắn một người dùng** — người dùng đó là chủ thể thực hiện mọi thao tác của key |
| **Toàn cục**     | Lấy access token (`/auth/partner-login`) | Không gắn tổ chức → **bắt buộc gửi `tenantCode`**                                                         |

***

## API Key

Mọi request tới Integration API gửi kèm header `X-API-Key`:

```http theme={null}
X-API-Key: econtract_a1b2c3d4e5f6...
Content-Type: application/json
```

<Warning>
  Chỉ tên header `X-API-Key` được chấp nhận (viết hoa thường tuỳ ý — HTTP không phân biệt). Alias cũ `apikey` **đã bị bỏ**: gửi bằng tên đó thì request bị coi như không có API Key và sẽ nhận `401` từ tầng xác thực JWT, chứ không phải lỗi API Key.
</Warning>

API Key được cấp khi onboard tổ chức và **chỉ hiển thị 1 lần**. Lưu trong biến môi trường hoặc secret manager, không commit vào source code.

### Lỗi liên quan tới API Key

Mọi lỗi đều trả `401` với `code: "UNAUTHORIZED"`; nguyên nhân nằm trong `message`:

| `message`                         | Nguyên nhân                                                                                                  |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| API key không hợp lệ              | Key sai, không tồn tại, sai định dạng, **đã bị thu hồi**, hoặc **đã hết hạn**                                |
| API key chưa gắn người dùng       | Đang dùng key toàn cục, hoặc key tổ chức chưa bind userId — quản trị viên gán qua Admin UI → API Keys → Edit |
| Tài khoản dịch vụ không hoạt động | User gắn với key đã bị vô hiệu hoá                                                                           |

<Note>
  Key bị thu hồi và key hết hạn trả về cùng thông báo với key sai — hệ thống không phân biệt ra ngoài. Chi tiết: [Mã lỗi](/api-reference/ma-loi).
</Note>

***

## Lấy Access Token — Partner Login

Khi bạn muốn nhúng giao diện econtractid (iframe trên web hoặc SDK mobile), người dùng cuối cần một **access token**. Backend của bạn dùng API Key để đổi lấy token cho một người dùng cụ thể:

```bash theme={null}
POST https://api.econtractid.com/api/auth/partner-login
x-api-key: econtract_a1b2c3d4...
Content-Type: application/json

{
  "username": "john.doe",
  "tenantCode": "ACME"
}
```

### Request

| Trường       | Kiểu     | Bắt buộc     | Mô tả                                                                                                                                               |
| ------------ | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `username`   | `string` | Có           | Username của người dùng cần lấy token                                                                                                               |
| `tenantCode` | `string` | Có điều kiện | Mã tổ chức. **Bắt buộc khi dùng API key toàn cục** (key không gắn tổ chức). Với key đã gắn tổ chức: bỏ qua được; nếu gửi thì phải khớp, sai → `403` |

<Warning>
  API Key **chỉ được đọc từ header `x-api-key`**, không bao giờ đặt trong body — thông tin nhạy cảm trong body dễ lọt vào access log và báo cáo lỗi.
</Warning>

### Response

Trả về HTTP `201` (mặc định của POST), body bọc trong envelope chuẩn:

```json theme={null}
{
  "statusCode": 200,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "userInfo": {
      "id": 1234,
      "username": "john.doe",
      "name": "Nguyễn Văn A",
      "email": "a@example.com",
      "phone": "0909123456",
      "identityNumber": "079...",
      "organizationId": -1,
      "status": 2,
      "type": "external"
    },
    "permission": []
  },
  "timestamp": "2026-07-20T08:00:00.000Z"
}
```

<Note>
  `userInfo` ở trên là bản **rút gọn**. Response thật còn có `address`, `avatar`, `memberOf`, `updatedAt`, và các trường chữ ký (`defaultSignature`, `imageSignatures`, `backgroundSignatures`).

  `status` là số (`1` = chờ kích hoạt, `2` = đang hoạt động). `organizationId` bằng `-1` với người dùng bên ngoài — đây là giá trị bình thường, không phải lỗi.
</Note>

<Warning>
  Response **không** trả về `refreshToken` và **không** trả về `expiresIn`. Xem mục **Vòng đời token** bên dưới.

  Lưu ý `statusCode` trong body luôn là `200` kể cả khi HTTP status là `201` — hãy dựa vào **HTTP status thật** của response, không dựa vào trường này.
</Warning>

### Lỗi

| HTTP  | Nguyên nhân                                                                                               |
| ----- | --------------------------------------------------------------------------------------------------------- |
| `400` | Thiếu `tenantCode` khi dùng key toàn cục; hoặc `tenantCode` trùng ở nhiều tổ chức nên không xác định được |
| `401` | API Key không hợp lệ                                                                                      |
| `403` | `tenantCode` không khớp tổ chức của API Key                                                               |
| `404` | Không tìm thấy tổ chức ứng với `tenantCode`, hoặc không tìm thấy người dùng                               |

### Giới hạn quan trọng

<Warning>
  **Endpoint này không tạo người dùng.** Username chưa tồn tại → trả `404`. Người ký phải được onboard trước qua luồng tạo hồ sơ thông thường (khai báo trong `signers[]` khi gọi `POST /documents`).

  Endpoint chỉ cấp token cho người dùng **là thành viên của tổ chức tương ứng** — cả người ký bên ngoài (external) lẫn nhân sự nội bộ (internal). Tài khoản **quản trị hệ thống (super admin) bị từ chối** (`403`), vì token này được cấp không qua mật khẩu.
</Warning>

<Warning>
  Token cấp qua `partner-login` **không cần mật khẩu**. Ai giữ API Key đều có thể lấy token của bất kỳ người dùng external nào trong tổ chức. API Key **tuyệt đối không được để lộ xuống trình duyệt hoặc ứng dụng di động** — chỉ gọi từ backend.
</Warning>

### Dùng access token gọi API

Token gửi qua header **`x-access-token`**, giá trị là JWT thuần:

```http theme={null}
x-access-token: eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
```

<Warning>
  **Không dùng `Authorization: Bearer <token>`.** Hệ thống chỉ đọc token từ header `x-access-token`, và **không** có tiền tố `Bearer`. Gửi sai tên header — hoặc thêm tiền tố `Bearer` — thì request bị coi như không có token và trả `401` mà không kèm mô tả.
</Warning>

```bash theme={null}
curl -i https://api.econtractid.com/api/integration/tasks \
  -H "x-access-token: $ACCESS_TOKEN"
```

Các endpoint Integration API nhận **cả hai** cách xác thực: `X-API-Key` (người dùng = tài khoản gắn với key) hoặc `x-access-token` (người dùng = chủ sở hữu token). Gửi kèm cả hai thì **API Key được ưu tiên**, token bị bỏ qua — nên chỉ gửi một.

#### `401` khi token trông vẫn hợp lệ

| Kiểm tra         | Chi tiết                                                                                        |
| ---------------- | ----------------------------------------------------------------------------------------------- |
| Tên header       | Phải là `x-access-token`. `Authorization: Bearer ...` → luôn `401`                              |
| Tiền tố          | Gửi JWT thuần, không có `Bearer ` phía trước                                                    |
| Khoảng trắng     | Token bị xuống dòng / thêm dấu cách khi copy làm hỏng chữ ký                                    |
| Đúng môi trường  | Token lấy ở môi trường dev **không** dùng được trên production và ngược lại — khoá ký khác nhau |
| Hạn dùng         | Giải mã phần payload, so `exp` (epoch giây) với hiện tại                                        |
| Nguyên vẹn       | Token gồm đúng 3 đoạn ngăn bởi dấu `.`; thiếu hoặc thừa một ký tự là chữ ký sai                 |
| Proxy trung gian | Một số CDN/API gateway lọc bỏ header lạ — kiểm tra bằng cách gọi thẳng API, không qua proxy     |

<Note>
  Tầng xác thực chỉ kiểm tra chữ ký và hạn dùng của token, **không** kiểm tra quyền. Nhận `401` nghĩa là token/header sai — vấn đề về quyền hạn trả `403`, còn dữ liệu không thuộc phạm vi người dùng trả `404`.
</Note>

***

## Vòng đời token

| Thuộc tính                 | Giá trị         |
| -------------------------- | --------------- |
| Thời hạn (production)      | **15 ngày**     |
| Endpoint làm mới (refresh) | **Không có**    |
| Thu hồi từng token         | Không hỗ trợ    |
| Thu hồi hàng loạt          | Thu hồi API Key |

Vì hệ thống **không có endpoint refresh**, luồng xử lý khi token hết hạn là **lấy token mới**:

```mermaid theme={null}
sequenceDiagram
    participant U as Người dùng
    participant P as Backend của bạn
    participant E as econtractid API

    P->>E: POST /auth/partner-login (x-api-key)
    E-->>P: accessToken
    P-->>U: Giao token cho iframe / SDK
    U->>E: Gọi API với token
    E-->>U: 401 (token hết hạn)
    U->>P: Yêu cầu token mới
    P->>E: POST /auth/partner-login
    E-->>P: accessToken mới
    P-->>U: Cập nhật token
```

<Warning>
  15 ngày là thời hạn dài. Hãy đối xử với access token như một thông tin xác thực:

  * Không ghi token vào log
  * Không lưu lâu dài ở phía client
  * Không nhúng token vào URL có thể bị chia sẻ, bookmark hoặc lưu vào lịch sử trình duyệt
  * Khi nghi ngờ lộ token: thu hồi API Key và cấp key mới
</Warning>

***

## Áp dụng vào tích hợp

<CardGroup cols={2}>
  <Card title="Nhúng web (iframe)" icon="window-restore" href="/technical/nhung-iframe">
    Truyền token qua query param `at`
  </Card>

  <Card title="SDK Flutter" icon="mobile-screen" href="/technical/sdk-flutter">
    Truyền token vào `openTaskList()`, làm mới bằng `updateAccessToken()`
  </Card>
</CardGroup>
