> ## 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.

# API Hồ sơ

> Tạo hồ sơ ký số, kích hoạt quy trình, tra cứu trạng thái và tải file

Trang này gồm 5 endpoint theo đúng vòng đời một hồ sơ ký.

| # | Endpoint                                             | Mục đích            |
| - | ---------------------------------------------------- | ------------------- |
| 1 | `POST /api/integration/documents`                    | Tạo hồ sơ ký số     |
| 2 | `POST /api/integration/documents/{documentId}/start` | Kích hoạt quy trình |
| 3 | `GET /api/integration/documents/{documentId}`        | Tra cứu trạng thái  |
| 4 | `GET /api/integration/documents/{documentId}/files`  | Liệt kê file        |
| 5 | `GET /api/integration/files/{fileUuid}/download`     | Tải file            |

Cần danh sách hồ sơ **cần ký / đã ký của một người dùng**? Xem [API Công việc ký](/api-reference/endpoint/cong-viec-ky).

***

## Trạng thái hồ sơ

```mermaid theme={null}
stateDiagram-v2
  [*] --> PENDING: POST /documents (disableStart true)
  [*] --> PENDING_SIGN: POST /documents (mặc định)
  PENDING --> PENDING_SIGN: POST /documents/:id/start
  PENDING_SIGN --> IN_PROGRESS: signer đầu tiên ký
  PENDING_SIGN --> REJECTED: signer từ chối
  IN_PROGRESS --> IN_PROGRESS: signer tiếp theo ký
  IN_PROGRESS --> COMPLETED: tất cả signer xong
  IN_PROGRESS --> REJECTED: signer từ chối
  PENDING_SIGN --> EXPIRED: quá hạn
  IN_PROGRESS --> EXPIRED: quá hạn
  COMPLETED --> [*]: webhook document.completed
  REJECTED --> [*]: webhook document.rejected
```

***

## 1. Tạo hồ sơ ký số

```http theme={null}
POST /api/integration/documents
```

### Request

```json theme={null}
{
  "documentId": "HD-2026-001",
  "documentTitle": "Hợp đồng dịch vụ — Nguyễn Văn A",
  "processDefCode": "QT-KY-HD-SEQ",
  "ecmClassCode": "Tai-lieu-hop-dong-lao-dong",
  "disableStart": false,
  "files": [
    { "source": "url", "fileUrl": "https://your-storage/file.pdf", "title": "Hợp đồng", "ecmClassCode": "Tai-lieu-hop-dong-lao-dong" },
    { "source": "template", "templateCode": "PHU-LUC", "templateData": { "amount": 100000 }, "title": "Phụ lục" }
  ],
  "attachments": [
    { "source": "url", "fileUrl": "https://your-storage/ban-dich.pdf", "title": "Bản dịch tiếng Anh", "ecmClassCode": "Tai-lieu-dinh-kem" }
  ],
  "signers": [
    { "stepCode": "STEP_SIGN_HR", "fullName": "Nguyễn Văn A", "identityNumber": "079...", "email": "a@x.com", "phone": "0909..." },
    { "stepCode": "STEP_SIGN_PARTNER", "fullName": "Công ty TNHH ABC", "taxCode": "0312345678", "email": "ke-toan@abc.com" },
    { "stepCode": "STEP_SIGN_MANAGER", "username": "longnt" }
  ],
  "metadata": {
    "contractValue": 100000,
    "department": "HR"
  }
}
```

### Trường cấp gốc

| Trường           | Kiểu      | Bắt buộc | Mô tả                                                                                                             |
| ---------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `documentId`     | `string`  | Có       | Mã hồ sơ do bạn cấp, duy nhất trong tổ chức. Là khoá idempotency, được echo lại trong webhook và response tra cứu |
| `documentTitle`  | `string`  | Không    | Tên hiển thị (tối đa 500 ký tự). Mặc định bằng `documentId`                                                       |
| `processDefCode` | `string`  | Có       | Mã quy trình đã cấu hình trong tổ chức của bạn                                                                    |
| `ecmClassCode`   | `string`  | Không    | Phân loại của **hồ sơ**. Không dùng làm phân loại cho file                                                        |
| `disableStart`   | `boolean` | Không    | Mặc định `false` → tạo xong tự chạy quy trình. `true` → hồ sơ ở trạng thái `PENDING`, phải gọi endpoint start sau |
| `files[]`        | `array`   | Có       | Tối thiểu 1. Tài liệu chính, sẽ được ký                                                                           |
| `attachments[]`  | `array`   | Không    | Tài liệu đính kèm, **không** tham gia ký. Cùng cấu trúc với `files[]`                                             |
| `signers[]`      | `array`   | Có       | Tối đa 50 người ký                                                                                                |
| `metadata`       | `object`  | Không    | Dữ liệu tuỳ ý, được echo lại trong webhook. Hệ thống tự thêm `tenantCode`                                         |

### Trường của `files[]` và `attachments[]`

| Trường         | Kiểu     | Bắt buộc              | Mô tả                                              |
| -------------- | -------- | --------------------- | -------------------------------------------------- |
| `source`       | `enum`   | Có                    | `url` (PDF có sẵn) hoặc `template` (sinh từ mẫu)   |
| `fileUrl`      | `string` | Khi `source=url`      | HTTPS, PDF, dưới 50MB                              |
| `templateCode` | `string` | Khi `source=template` | Mã mẫu tài liệu                                    |
| `templateData` | `object` | Không                 | Dữ liệu điền vào mẫu                               |
| `title`        | `string` | Không                 | Tên hiển thị (tối đa 255 ký tự)                    |
| `ecmClassCode` | `string` | Tuỳ                   | Phân loại riêng cho file này. Xem quy tắc bên dưới |

<Warning>
  **Quy tắc `ecmClassCode` cho từng file:** nếu quy trình chỉ ký **một loại** tài liệu, file tự kế thừa phân loại cấu hình trong quy trình. Nếu quy trình ký **từ hai loại trở lên**, bắt buộc gửi `ecmClassCode` cho từng file, thiếu sẽ báo lỗi `MISSING_ECM_CLASS_CODE`.

  `attachments[]` **không** kế thừa `ecmClassCode` từ cấp gốc — phải tự gửi.
</Warning>

<Note>
  **Kiểm tra `fileUrl`:** server tự tải file từ URL bạn cung cấp. Yêu cầu: HTTPS, dưới 50MB, phản hồi trong 30 giây, URL đọc được công khai (không cần header xác thực), **không qua redirect**, và không trỏ vào dải IP nội bộ.

  Hệ thống nhận diện PDF bằng **chữ ký nhị phân đầu file**, không dựa vào header `Content-Type`. Xem [mã lỗi 413/415/502](/api-reference/ma-loi).
</Note>

### Trường của `signers[]`

| Trường           | Kiểu      | Bắt buộc                   | Mô tả                                                                                             |
| ---------------- | --------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
| `stepCode`       | `string`  | Có                         | Khớp mã bước trong quy trình (tối đa 64 ký tự)                                                    |
| `fullName`       | `string`  | Không                      | Họ tên. Bỏ trống khi dùng `username` (hệ thống tự lấy từ hồ sơ người dùng)                        |
| `identityNumber` | `string`  | Không                      | Số CMND/CCCD/Hộ chiếu, 9–12 chữ số — dùng khi người ký là **cá nhân**                             |
| `taxCode`        | `string`  | Không                      | Mã số thuế, 10 chữ số (hoặc 10 + 3 chữ số chi nhánh) — dùng khi người ký là **tổ chức**           |
| `email`          | `string`  | Với bước khách mời         | Email người ký                                                                                    |
| `phone`          | `string`  | Với bước khách mời qua SĐT | Số điện thoại                                                                                     |
| `username`       | `string`  | Không                      | Username nội bộ. Khi có, hệ thống gán đích danh người này vào bước, bỏ qua phân phối theo vai trò |
| `order`          | `integer` | Không                      | Thứ tự ký khi nhiều người cùng một `stepCode`. Thiếu → theo vị trí trong mảng                     |

<Note>
  `identityNumber` và `taxCode` dùng để phân loại người ký: cá nhân dùng `identityNumber`, tổ chức dùng `taxCode`. **Chỉ gửi một trong hai.** Khi gửi `taxCode`, tài khoản người ký được tạo với username chính là mã số thuế.
</Note>

### Giới hạn độ dài và định dạng

| Trường                                         | Giới hạn                                                           |
| ---------------------------------------------- | ------------------------------------------------------------------ |
| `documentId`, `processDefCode`, `ecmClassCode` | tối đa 255 ký tự                                                   |
| `documentTitle`                                | tối đa 500 ký tự                                                   |
| `files[]`                                      | tối thiểu 1                                                        |
| `attachments[]`                                | tối đa 50                                                          |
| `signers[]`                                    | tối đa 50                                                          |
| `signers[].stepCode`                           | tối đa 64 ký tự                                                    |
| `signers[].fullName`, `signers[].username`     | tối đa 255 ký tự                                                   |
| `signers[].identityNumber`                     | 9–12 chữ số                                                        |
| `signers[].taxCode`                            | 10 chữ số, hoặc 10 chữ số + 3 chữ số chi nhánh (có thể có dấu `-`) |
| `signers[].phone`                              | 8–20 ký tự, chỉ gồm chữ số, `+`, `-`, khoảng trắng                 |

<Note>
  `username` bị **bỏ qua** (kèm cảnh báo trong log) nếu bước tương ứng là bước khách mời (`guest` / `phone_guest`) — những bước này định danh người ký qua email/số điện thoại.
</Note>

### Nhiều người ký cùng một bước

Nhiều signer trỏ vào cùng `stepCode` sẽ tạo nhiều lượt ký tại bước đó, diễn ra lần lượt theo `order` (hoặc thứ tự trong mảng). Hỗ trợ cả khách mời (mỗi người một email/định danh) lẫn nhân sự nội bộ (mỗi người một `username`):

```json theme={null}
"signers": [
  { "stepCode": "STEP_SIGN_CUSTOMER", "fullName": "Nguyễn Văn A", "identityNumber": "079...", "email": "a@x.com", "order": 1 },
  { "stepCode": "STEP_SIGN_CUSTOMER", "fullName": "Trần Thị B",   "identityNumber": "080...", "email": "b@x.com", "order": 2 },
  { "stepCode": "STEP_SIGN_CUSTOMER", "fullName": "Công ty ABC",  "taxCode": "0312345678",    "email": "c@x.com", "order": 3 },
  { "stepCode": "STEP_SIGN_INTERNAL", "username": "chuyenvien" },
  { "stepCode": "STEP_SIGN_INTERNAL", "username": "thamdinhvien" }
]
```

Trong response tra cứu và webhook, mỗi lượt ký được nhận diện qua định danh (email / `identityNumber` / `taxCode` / `username`), không qua `stepCode`.

### Response

```json theme={null}
{
  "statusCode": 200,
  "data": {
    "transactionId": "PI000042",
    "documentId": "HD-2026-001",
    "documentTitle": "Hợp đồng dịch vụ — Nguyễn Văn A",
    "folderId": 12345,
    "documentUuid": "8d5e7b2a-1c4f-4a3b-9d6e-2f8c7a1e5b9d",
    "files": [
      {
        "id": 678,
        "fileUuid": "f1a2b3c4-...",
        "title": "Hợp đồng",
        "type": "MAIN",
        "url": "https://.../signed-url.pdf?...",
        "downloadUrl": "/api/integration/files/f1a2b3c4-.../download"
      }
    ],
    "attachments": [],
    "status": "PENDING_SIGN",
    "createdAt": "2026-05-15T10:00:00Z"
  },
  "timestamp": "2026-05-15T10:00:00.123Z"
}
```

| Trường                | Mô tả                                                                                                       |
| --------------------- | ----------------------------------------------------------------------------------------------------------- |
| `transactionId`       | Mã tiến trình do hệ thống quy trình sinh (vd `PI000042`). `null` nếu chưa sinh. Được echo lại trong webhook |
| `documentUuid`        | UUID công khai của hồ sơ. Ổn định, bất biến                                                                 |
| `files[].fileUuid`    | UUID công khai của file. Dùng làm tham số cho endpoint tải file                                             |
| `files[].type`        | `MAIN` (tham gia ký) hoặc `ATTACHMENT` (đính kèm)                                                           |
| `files[].url`         | Presigned URL, mặc định hết hạn sau **24 giờ**                                                              |
| `files[].downloadUrl` | Đường dẫn tương đối tới endpoint tải file, **không hết hạn**                                                |

<Note>
  Response của endpoint tạo hồ sơ **không** chứa `ecmClassCode`, `size`, `mimeType` — các trường này chỉ có ở mục tra cứu hồ sơ và liệt kê file bên dưới.

  Với `disableStart: true`, `status` trả về là `PENDING` thay vì `PENDING_SIGN`.
</Note>

<Warning>
  Endpoint trả về HTTP `201`, nhưng `statusCode` trong body luôn ghi `200`. Dựa vào HTTP status thật, không dựa vào trường trong body.
</Warning>

### Xử lý phía server

Toàn bộ quá trình là **atomic** — bất kỳ bước nào lỗi thì rollback hoàn toàn, không để lại hồ sơ dở dang:

1. Xác thực API Key → xác định tổ chức
2. Kiểm tra idempotency theo `documentId` → trùng thì `409`
3. Tra cứu `processDefCode` → lấy quy trình và các bước
4. Xử lý file: `source=template` thì render từ mẫu; `source=url` thì tải PDF qua bộ lọc SSRF rồi lưu trữ
5. Kiểm tra và gán người ký theo `stepCode`
6. Tạo hồ sơ, lưu `metadata`
7. Khởi chạy quy trình, gửi thông báo cho người ký

### Ví dụ cURL

```bash theme={null}
curl -sS -X POST https://api.econtractid.com/api/integration/documents \
  -H "X-API-Key: econtract_a1b2c3d4..." \
  -H "Content-Type: application/json" \
  -d '{
    "documentId":"HD-2026-001",
    "documentTitle":"Hợp đồng dịch vụ — Nguyễn Văn A",
    "processDefCode":"HDLD-01",
    "ecmClassCode":"Folder-Hop-Dong-Lao-Dong",
    "files":[
      {"source":"url","fileUrl":"https://your-storage/file.pdf","title":"HD chính","ecmClassCode":"Tai-lieu-hop-dong-lao-dong"}
    ],
    "signers":[
      {"stepCode":"STEP_SIGN_HR","fullName":"Nguyễn Văn A","email":"a@x.com","phone":"0909123001","identityNumber":"100000111001"},
      {"stepCode":"STEP_SIGN_MANAGER","username":"longnt"}
    ],
    "metadata":{"clientRef":"HR-2026-001"}
  }'
```

***

## 2. Kích hoạt quy trình

```http theme={null}
POST /api/integration/documents/{documentId}/start
```

Chỉ dùng khi hồ sơ được tạo với `disableStart: true`. Endpoint chuyển hồ sơ từ `PENDING` sang `PENDING_SIGN` và sinh công việc đầu tiên.

**Khi nào cần:**

* Bạn muốn tạo hồ sơ trước để đội vận hành rà soát nội bộ, sau đó mới phát hành cho ký
* Cần hẹn giờ phát hành (vd chỉ bắt đầu ngoài giờ làm việc)

```bash theme={null}
curl -sS -X POST https://api.econtractid.com/api/integration/documents/HD-2026-001/start \
  -H "X-API-Key: econtract_a1b2c3d4..."
```

<Note>
  Lỗi: `404` nếu `documentId` không tồn tại; `400` nếu hồ sơ không ở trạng thái `PENDING` (đã chạy rồi, hoặc đã kết thúc). Gọi lần hai khi quy trình đang chạy sẽ trả `400`.
</Note>

***

## 3. Tra cứu trạng thái hồ sơ

```http theme={null}
GET /api/integration/documents/{documentId}
```

Dùng khi bỏ lỡ webhook hoặc cần kiểm tra trạng thái trước khi thao tác tiếp.

```json theme={null}
{
  "statusCode": 200,
  "data": {
    "documentId": "HD-2026-001",
    "documentUuid": "8d5e7b2a-1c4f-4a3b-9d6e-2f8c7a1e5b9d",
    "transactionId": "PI000042",
    "status": "COMPLETED",
    "signedCount": 2,
    "requiredCount": 2,
    "files": [
      {
        "id": 678,
        "fileUuid": "f1a2b3c4-...",
        "title": "Hợp đồng",
        "type": "MAIN",
        "url": "https://minio.../f1a2b3c4.pdf?X-Amz-Signature=...",
        "downloadUrl": "/api/integration/files/f1a2b3c4-.../download",
        "ecmClassCode": "Tai-lieu-hop-dong-lao-dong",
        "size": 234567,
        "mimeType": "application/pdf"
      }
    ],
    "attachments": [],
    "signers": [
      { "stepName": "Khách hàng ký", "stepCode": "customer",
        "email": "a@x.com", "phone": "0909123001", "identityNumber": "079209876543", "taxCode": null,
        "status": "SIGNED", "signedAt": "2026-07-10T04:53:51.644Z" },
      { "stepName": "Nhân sự nội bộ ký", "stepCode": "internal_user",
        "username": "longnt",
        "status": "SIGNED", "signedAt": "2026-07-10T04:55:36.327Z" },
      { "stepName": "Trưởng phòng duyệt", "stepCode": "manager_approve",
        "roleCode": "TRUONG_PHONG", "roleName": "Trưởng phòng",
        "status": "PENDING", "signedAt": null }
    ],
    "metadata": { "tenantCode": "T001", "contractValue": 100000 },
    "createdAt": "2026-05-15T10:00:00Z",
    "completedAt": "2026-07-10T04:55:36.327Z"
  },
  "timestamp": "2026-07-10T05:00:00.000Z"
}
```

`requiredCount` là tổng số bước ký, `signedCount` là số bước đã ký. Hồ sơ chuyển `COMPLETED` khi hai giá trị bằng nhau.

### Định danh người ký

Mỗi item trong `signers[]` chỉ chứa các trường ứng với loại bước ký của nó:

| Loại bước            | Trường định danh                              | Ghi chú                                                                                          |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Khách mời (external) | `email`, `phone`, `identityNumber`, `taxCode` | Cá nhân dùng `identityNumber`, tổ chức dùng `taxCode` — chính là thông tin bạn gửi khi tạo hồ sơ |
| Nhân sự nội bộ       | `username`                                    | Không trả email/phone/định danh cá nhân                                                          |
| Bước theo vai trò    | `roleCode`, `roleName`                        | Khi đã có người ký, kèm thêm `username` của người ký thực tế                                     |

<Note>
  API không trả ID số nội bộ (`userId`, `roleId`) — nhất quán với nguyên tắc codes-first. Định danh ổn định là `username` (người dùng) và `roleCode` (vai trò).
</Note>

***

## 4. Liệt kê file của hồ sơ

```http theme={null}
GET /api/integration/documents/{documentId}/files
```

Trả về toàn bộ file con (`MAIN` + `ATTACHMENT`) kèm `fileUuid`. Dùng khi chỉ cần danh sách file mà không cần toàn bộ trạng thái hồ sơ.

```json theme={null}
{
  "statusCode": 200,
  "data": {
    "documentId": "HD-2026-001",
    "documentUuid": "8d5e7b2a-1c4f-4a3b-9d6e-2f8c7a1e5b9d",
    "files": [
      {
        "id": 678,
        "fileUuid": "f1a2b3c4-...",
        "title": "Hợp đồng",
        "type": "MAIN",
        "url": "https://minio.../f1a2b3c4.pdf?X-Amz-Signature=...",
        "downloadUrl": "/api/integration/files/f1a2b3c4-.../download",
        "ecmClassCode": "Tai-lieu-hop-dong-lao-dong",
        "size": 234567,
        "mimeType": "application/pdf"
      }
    ],
    "attachments": []
  },
  "timestamp": "2026-05-15T10:00:00.123Z"
}
```

<Tip>
  Mỗi file có **hai cách lấy nội dung**: `url` (presigned, trỏ tới phiên bản mới nhất, **có thể hết hạn**) và `downloadUrl` (proxy qua API Key, **không hết hạn**). Nếu hệ thống của bạn xử lý file trễ hơn vài phút, dùng `downloadUrl`.
</Tip>

***

## 5. Tải file

```http theme={null}
GET /api/integration/files/{fileUuid}/download
```

Server stream trực tiếp nội dung file về — không redirect, không qua presigned URL trung gian. Lấy `fileUuid` từ response tạo hồ sơ, liệt kê file, tra cứu hồ sơ, hoặc từ webhook (`files[].ecmNodeUuid`).

**Đặc điểm:**

* Gọi được ở bất kỳ trạng thái nào của hồ sơ, mỗi lần lấy phiên bản mới nhất
* File thuộc tổ chức khác trả `404 FILE_NOT_FOUND` (không lộ sự tồn tại)

**Response headers:**

| Header                | Giá trị                                     |
| --------------------- | ------------------------------------------- |
| `Content-Type`        | `application/pdf` (hoặc mime type của file) |
| `Content-Disposition` | `attachment; filename="..."`                |
| `Content-Length`      | Kích thước theo byte                        |
| `Cache-Control`       | `private, no-store`                         |

```bash theme={null}
curl -sS -H "X-API-Key: econtract_a1b2c3d4..." \
  -o hop-dong.pdf \
  https://api.econtractid.com/api/integration/files/f1a2b3c4-1c4f-4a3b-9d6e-2f8c7a1e5b9d/download
```

***

## Bước tiếp theo

<CardGroup cols={2}>
  <Card title="Webhook" icon="bell" href="/api-reference/webhook">
    Nhận thông báo khi hồ sơ hoàn tất hoặc bị từ chối
  </Card>

  <Card title="Mã lỗi" icon="triangle-exclamation" href="/api-reference/ma-loi">
    Danh sách đầy đủ mã lỗi và cách xử lý
  </Card>
</CardGroup>
