# ข้อมูลอ้างอิง API (API REFERENCE)

## แผนภาพระบบ (Service Diagram)

```mermaid
flowchart LR
  subgraph financial_institution["เครือข่ายของสถาบันการเงิน (on-prem)"]
    direction LR
    user["ผู้ใช้ฝ่ายธุรกิจ<br/>เบราว์เซอร์"]
    wf["ระบบอื่น เช่น<br/>Thinker Workflow"]

    subgraph td["Thinker Docs"]
      api["/v1/generate<br/>/v1/templates<br/>/v1/documents"]
      eng["เครื่องมือเรนเดอร์<br/>+ fontkit"]
      qpdf["qpdf<br/>ใส่รหัสผ่าน AES-256"]
    end

    db[("PostgreSQL<br/>เทมเพลต · เวอร์ชัน · audit")]
    obj[("Object storage<br/>PDF ต้นฉบับ + เอกสารที่สร้าง")]
  end

  user -- "เซสชัน (คุกกี้)" --> api
  wf -- "X-Api-Key" --> api
  api --> eng --> qpdf
  api <--> db
  api <--> obj

  note["ไม่มีข้อมูลใดออกนอกเครือข่ายสถาบันการเงิน<br/>PII อยู่ในคำขอและในไฟล์ที่เก็บเท่านั้น — ไม่เคยอยู่ใน log"]
  td -.- note
```


## ลำดับการทำงาน (Sequence Diagram)

### sync — ได้รับไฟล์ PDF กลับมาทันที

```mermaid
sequenceDiagram
  autonumber
  participant C as ระบบที่เรียก
  participant A as Thinker Docs API
  participant D as PostgreSQL
  participant R as เครื่องมือเรนเดอร์

  C->>A: GET /v1/templates/:id/fields?version=2
  A-->>C: 200 สัญญาฟิลด์ (ส่งอะไรบ้าง)
  C->>A: POST /v1/generate (mode "sync", data)
  A->>A: ตรวจ data กับสัญญา — ผิด → 400 invalid_data
  A->>D: อ่านเทมเพลตเวอร์ชันที่ระบุ
  A->>R: เรนเดอร์
  A->>D: เขียน audit_log (ไม่เก็บ data)
  A-->>C: 200 application/pdf ← ไฟล์ · ไม่เก็บไว้เลย
```

### async — ได้ลิงก์ (ไฟล์ถูกเก็บไว้ให้)

```mermaid
sequenceDiagram
  autonumber
  participant C as ระบบที่เรียก
  participant A as Thinker Docs API
  participant S as Object storage
  participant D as PostgreSQL

  C->>A: POST /v1/generate (mode "async", data)
  A->>A: เรนเดอร์ + คำนวณ sha256
  A->>S: เก็บไฟล์ PDF
  A->>D: บันทึกแถว documents (มีวันหมดอายุ) + audit_log
  A-->>C: 201 { documentId, url, sha256, expiresAt }
  Note over C,A: url มีอายุจำกัด — อย่าเก็บ<br/>ให้เก็บ documentId
  C->>A: GET /v1/documents/:id  (เมื่อไหร่ก็ได้)
  A-->>C: 200 พร้อม url ที่ออกใหม่
  Note over A,S: เลย expiresAt แล้ว → 410 Gone<br/>ไฟล์ถูกลบ แต่ audit row ยังอยู่
```

### attach — รวมเอกสาร แล้วเลขหน้าถูกคำนวณใหม่

```mermaid
sequenceDiagram
  autonumber
  participant C as ระบบที่เรียก
  participant A as Thinker Docs API

  C->>A: POST /v1/generate (mode "async") — สำเนาบัตร 2 หน้า
  A-->>C: 201 { documentId: "ID-SCAN" }
  C->>A: POST /v1/generate (สัญญา 2 หน้า, options.attach ["ID-SCAN"])
  A->>A: เรนเดอร์สัญญา (พักหัว/ท้ายกระดาษไว้ก่อน)
  A->>A: ตรวจว่า ID-SCAN เป็นของทีมนี้ — ถ้าไม่ใช่ → 404
  A->>A: รวมหน้า → 4 หน้า
  A->>A: วาดลายน้ำ + เลขหน้าทับ "เอกสารสุดท้าย"
  A-->>C: เอกสาร 4 หน้า · ท้ายหน้า 3 เขียนว่า "หน้า 3 จาก 4"
```

---

## 1. การพิสูจน์ตัวตน (Authentication), ขอบเขต (Scopes) และการแยกข้อมูลตามทีม (Team Isolation)
การเข้าถึง API ทุกส่วนใช้รหัสกุญแจ API (API Key) ส่งผ่านส่วนหัว (Header) ของคำขอ HTTP:
```http
X-Api-Key: tdk_XXXXXXXXXXXXXXXXXXXXXXXX
```

### ขอบเขตการใช้งาน (Scopes)
กุญแจ API แต่ละชุดจะถูกจำกัดสิทธิ์ตามขอบเขตความปลอดภัย (Least Privilege) ดังนี้:
*   `generate`: อนุญาตให้เรียกสร้างเอกสารที่ `/v1/generate` เท่านั้น
*   `documents:read`: อนุญาตให้เรียกดูหรือดาวน์โหลดเอกสารจาก `/v1/documents/:id` เท่านั้น
*   `templates:read`: อนุญาตให้ดึงโครงสร้างตัวแปรข้อตกลงผ่าน `/v1/templates/:id/fields`
*   `identity-sync`: อนุญาตเฉพาะสำหรับ API ซิงค์ข้อมูลทีมและผู้ใช้ที่ `/v1/sync/identities` เท่านั้น (ห้ามรวมขอบเขตนี้ร่วมกับขอบเขตอื่นๆ)

### การแยกข้อมูลตามทีม (Team Isolation)
กุญแจ API ทุกชุดจะผูกมัดเข้ากับหน่วยงานหรือทีม (`teamId`) อย่างเข้มงวด:
*   หากมีการพยายามดึงข้อมูลเทมเพลตหรือเอกสารของทีมอื่น ระบบจะส่งรหัสตอบกลับ **`404 Not Found`** เสมอ (ไม่ใช้ `403 Forbidden` เพื่อป้องกันผู้ใช้งานสุ่มหาไอดีเอกสารของต่างทีม)
*   คำขอที่เข้าข่ายข้ามสิทธิ์ผู้ใช้จะลงบันทึกในตารางตรวจสอบเป็นสถานะ `outcome: denied` เพื่อประโยชน์ด้านความมั่นคงปลอดภัย
*   กุญแจ API ทำหน้าที่เป็นอัตลักษณ์ของเครื่องทำงาน (Machine Identity) ที่ผูกติดกับทีม ไม่ได้ขึ้นอยู่กับสถานะของตัวบุคคลผู้สร้างคีย์ แม้พนักงานลาออกก็ไม่ส่งผลกระทบต่อระบบงานที่ผสานการทำงานอยู่

---

## 2. ข้อมูลตัวแปรของเทมเพลต (Field Discovery)
ค้นหาข้อมูลรายละเอียดของฟิลด์ตัวแปรที่เทมเพลตต้องการเพื่อนำมาใช้กรอกข้อมูล
*   **สิทธิ์ที่ต้องใช้**: ขอบเขต `templates:read` (หรือล็อกอินด้วยสิทธิ์ Editor/Admin)
*   **เส้นทาง (Route)**: `GET /v1/templates/:id/fields`

### พารามิเตอร์คิวรี (Query Parameters)
*   `version` (ตัวเลข, ทางเลือก): ระบุเวอร์ชันเทมเพลตที่เผยแพร่แล้ว หากละเว้นจะแสดงข้อมูลของเวอร์ชันเผยแพร่ล่าสุด
*   `draft` (ค่าจริง/เท็จ, ทางเลือก): ส่ง `true` เพื่อดึงข้อมูลของรุ่นร่างปัจจุบัน (ใช้งานได้เฉพาะเซสชันคุกกี้ของผู้ใช้ออกแบบที่มีบทบาท Editor/Admin เท่านั้น กุญแจ API ไม่ได้รับอนุญาตให้เข้าถึงแบบร่าง)

### ตัวอย่างผลลัพธ์ (Response Payload)
```json
{
  "templateId": "tpl_7f3a",
  "version": 1,
  "status": "published",
  "fields": [
    {
      "name": "customerName",
      "type": "text",
      "required": true
    },
    {
      "name": "loanAmount",
      "type": "number",
      "required": true,
      "format": {
        "decimals": 2,
        "thousandsSeparator": true,
        "prefix": "฿ "
      },
      "validation": {
        "min": 0
      }
    },
    {
      "name": "contractDate",
      "type": "date",
      "required": true,
      "format": {
        "calendar": "buddhist",
        "pattern": "d MMMM yyyy"
      }
    }
  ]
}
```

### ชนิดฟิลด์ (`type`) และรูปแบบค่าที่ต้องส่งใน `data`
Field Discovery จะบอก `type` ของแต่ละฟิลด์ ค่าที่ส่งเข้าไปใน `data` ต้องตรงตามชนิดนั้น หากไม่ตรงจะได้รับ `400 invalid_data` พร้อมรายการฟิลด์ที่บกพร่อง:

| ชนิด (`type`) | รูปแบบค่าที่ส่ง | ข้อจำกัด / หมายเหตุ |
|---|---|---|
| `text` | ข้อความ (string) | อักขระควบคุมจะถูกตัดออกอัตโนมัติ |
| `number` | ตัวเลข หรือสตริงตัวเลข เช่น `1250000` หรือ `"1250000"` | แสดงผลตาม `format`/`validation` ของฟิลด์ |
| `date` | สตริงวันที่แบบ ISO เช่น `"2026-07-21"` | แสดงผลตาม `format` (รองรับปฏิทินพุทธ) |
| `image` | **Data URL เท่านั้น** เช่น `"data:image/png;base64,iVBORw0KG..."` | รองรับ PNG/JPEG/GIF/WebP • ปฏิเสธ URL แบบ `http(s)` • ขนาดภาพต้องไม่เกิน 30 ล้านพิกเซล (กว้าง×สูง) เกินคืน `invalid_data` |
| `qrcode` | ข้อความที่จะเข้ารหัสเป็น QR เช่น `"https://example.com/verify/abc"` | — |
| `table` | **อาเรย์ 2 มิติของข้อความ** เช่น `[["Alice","New York"],["Bob","LA"]]` หรือ **สตริง JSON** ของอาเรย์นั้น | แต่ละอาเรย์ย่อยคือหนึ่งแถว • จำกัดสูงสุด 2,000 แถว และ 20,000 เซลล์รวม เกินคืน `invalid_data` |
| `svg` | **มาร์กอัป SVG แบบอินไลน์** เช่น `"<svg xmlns='http://www.w3.org/2000/svg' ...>...</svg>"` (ต้องมี `<svg` และ `</svg>` — **ไม่ใช่** data URL) | เพื่อความปลอดภัย ระบบปฏิเสธ `<script>`, ตัวจัดการเหตุการณ์ `on*=`, `<!DOCTYPE>`/`<!ENTITY>`/`<?xml-stylesheet`, และการอ้างอิงทรัพยากรภายนอก (`href`/`xlink:href`/`src`) เกินคืน `invalid_data` |

> ค่าที่ส่งต้องครอบคลุมทุกฟิลด์ที่ `required: true` หากฟิลด์ใดชนิด `table`/`svg`/`image` ปรากฏใน Field Discovery แต่ `data` ส่งค่าผิดชนิด (เช่น ส่งข้อความธรรมดาให้ `table`) จะถูกปฏิเสธด้วย `invalid_data`

---

## 3. การสร้างเอกสาร (Generate)
สั่งกรอกข้อมูลลงเทมเพลตเพื่อผลิตไฟล์ PDF
*   **สิทธิ์ที่ต้องใช้**: ขอบเขต `generate`
*   **เส้นทาง (Route)**: `POST /v1/generate`
*   **ประเภทข้อมูล**: `Content-Type: application/json` (ขนาดตัวขอต้องไม่เกิน 1 MB)

### โครงสร้างพารามิเตอร์ใน Body
*   `templateId` (ข้อความ, จำเป็น): รหัสประจำตัวเทมเพลต
*   `version` (ตัวเลข, ทางเลือก): ระบุเวอร์ชันเผยแพร่ที่ต้องการล็อกการผลิต หากละไว้จะใช้รุ่นล่าสุดของระบบ
*   `mode` (ข้อความ, จำเป็น): ตัวเลือกระหว่าง `"sync"` (ประสานเวลา) หรือ `"async"` (ไม่ประสานเวลา)
*   `data` (วัตถุ JSON, จำเป็น): คีย์-ค่าของตัวแปรตามข้อกำหนดใน Field Discovery
*   `options` (วัตถุ JSON, ทางเลือก):
    *   `metadata` (วัตถุ JSON, ทางเลือก): ข้อมูลระบุรายละเอียดเอกสารที่จะฝังใน PDF (ห้ามใส่ PII)
        *   `title`: ชื่อเรื่องเอกสาร (จำกัด 200 ตัวอักษร)
        *   `subject`: หัวข้อเอกสาร (จำกัด 200 ตัวอักษร)
    *   `protection` (วัตถุ JSON, ทางเลือก): ข้อมูลการตั้งรหัสผ่านไฟล์ PDF (ต้องการระบบ qpdf บนเซิร์ฟเวอร์)
        *   `userPassword`: รหัสผ่านสำหรับการเปิดอ่านเอกสาร (จำเป็นต้องกรอกอย่างน้อยหนึ่งรหัสผ่าน)
        *   `ownerPassword`: รหัสผ่านสำหรับจัดการสิทธิ์แก้ไขเอกสาร (ถ้าละไว้จะเท่ากับ userPassword)
        *   `permissions`: ระบุอนุญาตความปลอดภัย `{ "print": true, "copy": false, "modify": false }`
    *   `attach` (อาเรย์ข้อความ, ทางเลือก): รายการไอดี `documentId` ของเอกสารของทีมเดียวกันที่สร้างผ่านโหมด Async เพื่อนำมาเชื่อมต่อรวมแฟ้ม PDF (แนบได้สูงสุด 3 ฉบับ)
    *   `watermark` (วัตถุ JSON, ทางเลือก): ข้อความลายน้ำพิมพ์ทับบนเอกสาร
        *   `text`: ข้อความลายน้ำ เช่น "ฉบับร่าง" (จำกัดไม่เกิน 30 ตัวอักษร)
        *   `opacity`: ระดับความโปร่งแสง (ค่า 0 ถึง 1)
    *   `bulkOutput` (ข้อความ, ทางเลือก): กำหนดการส่งออกแบบกลุ่ม (ไม่รองรับในรุ่นนี้ ส่งค่าเข้ามาจะคืนข้อผิดพลาด `501 not_implemented` ทันที)

### รูปแบบคำตอบรับ (Responses)
*   **โหมด Sync (`mode: "sync"`)**: คืนค่าตอบกลับเป็นไบนารีไฟล์ PDF ดิบพร้อมรหัสสถานะ `200 OK`
    *   `Content-Type: application/pdf`
*   **โหมด Async (`mode: "async"`)**: คืนค่าคำรับเรื่องสร้างเอกสารเป็นรหัสสถานะ `201 Created`
    ```json
    {
      "documentId": "8108147a-9f5b-426c-8e01-c092d8f921ab",
      "url": "https://docs.thinker.internal/v1/documents/8108147a-9f5b-426c-8e01-c092d8f921ab?download=1",
      "sha256": "c092d8f921ab4cd9f257a627e31b40283c7dfa2c2c0183b9cfad83c0f829f0e1",
      "expiresAt": "2026-08-12T17:00:00.000Z"
    }
    ```

---

## 4. การดึงข้อมูลเอกสาร (Retrieve Async Document)
เรียกดูข้อมูลหรือดาวน์โหลดไฟล์ที่สั่งสร้างไว้ผ่านโหมด Async
*   **สิทธิ์ที่ต้องใช้**: ขอบเขต `documents:read`
*   **เส้นทาง (Route)**: `GET /v1/documents/:id`

### พารามิเตอร์คิวรี (Query Parameters)
*   `download` (ตัวเลข, ทางเลือก): หากระบุเป็น `1` ระบบจะทำการสตรีมส่งไฟล์ PDF ไบนารีกลับมาโดยตรง (รหัสสถานะ `200 OK` และระบุ `Content-Type: application/pdf`)

### ตัวอย่างผลลัพธ์กรณีปกติ (ไม่ใช่ดาวน์โหลด)
```json
{
  "documentId": "8108147a-9f5b-426c-8e01-c092d8f921ab",
  "templateId": "tpl_7f3a",
  "version": 1,
  "sha256": "c092d8f921ab4cd9f257a627e31b40283c7dfa2c2c0183b9cfad83c0f829f0e1",
  "createdAt": "2026-07-14T00:07:05.000Z",
  "expiresAt": "2026-08-14T00:07:05.000Z",
  "url": "https://docs.thinker.internal/v1/storage/documents/8108147a-9f5b-426c-8e01-c092d8f921ab?token=..."
}
```

---

## 5. รหัสข้อผิดพลาดและ HTTP สถานะ (Error Codes & HTTP Statuses)

| HTTP สถานะ | รหัสข้อผิดพลาด (code) | คำอธิบายสาเหตุและแนวทางแก้ไข |
|---|---|---|
| **400 Bad Request** | `bad_request` | โครงสร้าง JSON ผิดพลาด, ส่งค่าตัวเลือกไม่ตรงตามเงื่อนไข (เช่น ไม่ระบุรหัสผ่านในฟังก์ชัน protection หรือระบุ mode ไม่ถูกต้อง) |
| **400 Bad Request** | `invalid_data` | ข้อมูลตัวแปรไม่เป็นไปตามข้อตกลงของฟิลด์ คืนข้อมูลรายการฟิลด์ที่บกพร่องในตัวแปร `fields` เช่น `[{"field": "loanAmount", "reason": "expected a number"}]` |
| **401 Unauthorized** | `unauthenticated` | ไม่พบหรือรหัสกุญแจ API คีย์ผิดพลาดในส่วนหัว `X-Api-Key` |
| **403 Forbidden** | `insufficient_scope` | กุญแจ API คีย์ไม่มีสิทธิ์ตามขอบเขตที่เรียกใช้ |
| **403 Forbidden** | `draft_requires_user` | มีความพยายามดึงข้อมูลหรือสร้างงานแบบร่างโดยใช้กุญแจ API (ระบบอนุญาตสิทธิ์นี้เฉพาะผู้ใช้ผ่านเว็บเบราว์เซอร์เท่านั้น) |
| **403 Forbidden** | `forbidden` | สิทธิ์ระดับบุคคลไม่เพียงพอ (เช่น บัญชีประเภท Viewer พยายามขอดึงข้อมูลแบบร่าง) |
| **404 Not Found** | `not_found` | ไม่พบเทมเพลตหรือเอกสาร หรือไฟล์ดังกล่าวอยู่ในความครอบครองของทีมอื่น |
| **410 Gone** | `gone` | เอกสารที่ระบุหมดอายุการเก็บรักษาและถูกทำลายออกจากระบบออนเพรมของสถาบันการเงินแล้ว |
| **413 Payload Too Large** | `payload_too_large` | ขนาดคำขอ HTTP เกินความจุที่อนุญาตไว้คือ 1 MB |
| **422 Unprocessable** | `render_failed` | ตัวประมวลผลเทมเพลตล้มเหลว (เกิดปัญหาจากโครงสร้างของตัวเทมเพลต ไม่ใช่ปัญหาเรื่องตัวแปรคำขอส่งเข้ามา) — รวมถึงกรณีการเรนเดอร์ใช้เวลานานเกินกำหนดแล้วถูกตัดจบ (timeout) |
| **429 Too Many Requests** | `rate_limited` | เรียก `generate` ถี่เกินเพดาน (ค่าเริ่มต้น **120 คำขอ/นาที ต่อกุญแจ API**) — คืนส่วนหัว `Retry-After` (วินาที) ให้รอแล้วค่อยลองใหม่ |
| **501 Not Implemented** | `not_implemented` | ส่งคำขอใช้งานความสามารถที่สเปกระบุแต่ระบบยังไม่ได้พัฒนาขึ้นมาในรุ่นนี้ (เช่น การพิมพ์แบบกลุ่ม bulkOutput) |
| **503 Service Unavailable** | `protection_unavailable` | มีคำขอเข้ารหัสผ่าน PDF แต่เซิร์ฟเวอร์ไม่มีการติดตั้งเครื่องมือ `qpdf` เอาไว้ (เป็นภาวะถาวรของเครื่องนั้น — **ไม่ควรลองใหม่**) |
| **503 Service Unavailable** | `overloaded` | เซิร์ฟเวอร์กำลังเรนเดอร์ PDF เต็มกำลัง (ถึงเพดานงานที่ทำพร้อมกัน) — เป็นภาวะ **ชั่วคราว** คืนส่วนหัว `Retry-After: 2` ให้ลองใหม่หลังหน่วงเวลา แนะนำใช้ exponential backoff **แยกให้ออกจาก `protection_unavailable`: ดูที่ค่า `code` ไม่ใช่แค่สถานะ 503** |

---

## 6. ตัวอย่างการใช้คำสั่ง Curl สำหรับทดสอบเชื่อมต่อ (Curl Integration Example)
นักพัฒนาสามารถคัดลอกชุดคำสั่งด้านล่างไปใช้รันเพื่อทดสอบเชื่อมระบบเบื้องต้น โดยเปลี่ยนค่าตัวแปรในระบบตามความเหมาะสม:

```bash
# 1. การเรียกตรวจสอบข้อมูลฟิลด์ที่จำเป็น (Field Discovery)
curl -s "https://docs.thinker.internal/v1/templates/tpl_7f3a/fields?version=1" \
  -H "X-Api-Key: $THINKER_DOCS_API_KEY"

# 2. การสั่งสร้างเอกสารแบบไม่ประสานเวลา (Async Document Generation)
# จะได้รับข้อมูล documentId และเส้นทางการดึงไฟล์
curl -s -X POST "https://docs.thinker.internal/v1/generate" \
  -H "X-Api-Key: $THINKER_DOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "tpl_7f3a",
    "version": 1,
    "mode": "async",
    "data": {
      "customerName": "นายสมชาย ใจดี",
      "loanAmount": 1250000,
      "contractDate": "2026-07-14",
      "refCode": "LN-2569-004182"
    },
    "options": {
      "metadata": {
        "title": "สัญญาเงินกู้รายย่อย LN-004182",
        "subject": "loan_ref=LN-2569-004182"
      },
      "protection": {
        "userPassword": "$PDF_PASSWORD",
        "permissions": {
          "print": true,
          "copy": false,
          "modify": false
        }
      }
    }
  }'

# 3. การดาวน์โหลดไฟล์ PDF ไบนารีด้วย documentId
curl -s "https://docs.thinker.internal/v1/documents/8108147a-9f5b-426c-8e01-c092d8f921ab?download=1" \
  -H "X-Api-Key: $THINKER_DOCS_API_KEY" \
  -o contract.pdf
```

---
---
---

---
## ตัวอย่างคำขอครบทุกแบบ (Request / curl)

ทุกตัวอย่างด้านล่างถูกทดสอบจริงกับระบบที่รันอยู่ ไม่ได้เขียนขึ้นจากการคาดเดา

```bash
BASE=https://docs.thinker.internal
TID=50e3a8aa-d4da-4c06-9d40-afeae1ad9c9b
# ห้ามใส่คีย์จริงลงในสคริปต์ที่ commit — อ่านจาก environment เท่านั้น
export THINKER_DOCS_API_KEY=...
```

### 1) ถามสัญญาฟิลด์ — ส่งอะไรบ้าง

```bash
curl -s "$BASE/v1/templates/$TID/fields?version=2"   -H "X-Api-Key: $THINKER_DOCS_API_KEY"
```

### 2) sync — ได้รับไฟล์ PDF กลับมาทันที

```bash
curl -s -X POST "$BASE/v1/generate"   -H "X-Api-Key: $THINKER_DOCS_API_KEY"   -H 'Content-Type: application/json'   -d '{
    "templateId": "'"$TID"'",
    "version": 2,
    "mode": "sync",
    "data": {
      "case_id": "LN-2569-001",
      "fullName": "นายหนึ่ง ในใจ",
      "nationalId": "1579940596522",
      "age": "40",
      "branch": "สำนักงานใหญ่"
    }
  }'   --output document.pdf
```

**สิ่งที่ได้กลับ**

```
HTTP/1.1 200 OK
content-type: application/pdf
content-length: 124559
```

### 3) async — ได้ลิงก์ (ไฟล์ถูกเก็บไว้ให้)

```bash
curl -s -X POST "$BASE/v1/generate"   -H "X-Api-Key: $THINKER_DOCS_API_KEY"   -H 'Content-Type: application/json'   -d '{
    "templateId": "'"$TID"'",
    "version": 2,
    "mode": "async",
    "data": { "case_id": "LN-2569-001", "fullName": "นายหนึ่ง ในใจ" }
  }'
```

**สิ่งที่ได้กลับ**

```json
{
  "documentId": "7ee7f983-d06c-49bf-bad8-d7168051a503",
  "url": "https://storage.internal/thinker-docs/documents/7ee7f983-….pdf?X-Amz-Signature=…",
  "sha256": "e4aec0e72ba031a731983d771b1714882eeb9dcbdb900bcf042a0c42087d3615",
  "expiresAt": "2026-08-12T17:15:21.009Z"
}
```

### 4) ดึงเอกสารที่เก็บไว้ (ขอลิงก์ใหม่ได้เสมอ)

```bash
DOCID=7ee7f983-d06c-49bf-bad8-d7168051a503

# แบบที่ 1 — รับ JSON พร้อม url ที่ออกใหม่
curl -s "$BASE/v1/documents/$DOCID" -H "X-Api-Key: $THINKER_DOCS_API_KEY"

# แบบที่ 2 — ให้ส่งไฟล์กลับมาโดยตรง
curl -s "$BASE/v1/documents/$DOCID?download=1"   -H "X-Api-Key: $THINKER_DOCS_API_KEY" -o out.pdf
```

### 5) ตั้งชื่อเอกสาร (metadata)

```bash
curl -s -X POST "$BASE/v1/generate"   -H "X-Api-Key: $THINKER_DOCS_API_KEY" -H 'Content-Type: application/json'   -d '{
    "templateId": "'"$TID"'", "version": 2, "mode": "async",
    "data": { "case_id": "LN-2569-001" },
    "options": {
      "metadata": { "title": "สัญญาเงินกู้ LN-2569-001", "subject": "case=LN-2569-001" }
    }
  }'
```

> ⚠️ metadata ไม่เคยถูกเข้ารหัส ใส่ได้แค่ **ตัวอ้างอิง** ห้ามใส่ชื่อลูกค้า เลขบัตร หรือจำนวนเงิน

### 6) ใส่รหัสผ่านให้ไฟล์ของผู้รับ (protection)

```bash
# รหัสผ่านมาจาก environment เท่านั้น — อย่าพิมพ์ลงคำสั่ง อย่าเก็บลงไฟล์
curl -s -X POST "$BASE/v1/generate"   -H "X-Api-Key: $THINKER_DOCS_API_KEY" -H 'Content-Type: application/json'   -d '{
    "templateId": "'"$TID"'", "version": 2, "mode": "sync",
    "data": { "case_id": "LN-2569-001" },
    "options": {
      "protection": {
        "userPassword": "'"$PDF_PASSWORD"'",
        "permissions": { "print": true, "copy": false, "modify": false }
      }
    }
  }' --output protected.pdf
```

ถ้าเซิร์ฟเวอร์ไม่มี `qpdf` จะได้ **503 `protection_unavailable`** — ไม่ใช่ไฟล์ที่ไม่ได้ล็อก

### 7) แนบเอกสารอื่นต่อท้าย + ลายน้ำ

```bash
curl -s -X POST "$BASE/v1/generate"   -H "X-Api-Key: $THINKER_DOCS_API_KEY" -H 'Content-Type: application/json'   -d '{
    "templateId": "'"$TID"'", "version": 2, "mode": "async",
    "data": { "case_id": "LN-2569-001" },
    "options": {
      "attach": ["7ee7f983-d06c-49bf-bad8-d7168051a503"],
      "watermark": { "text": "ฉบับร่าง", "opacity": 0.15 }
    }
  }'
```

`attach` รับได้เฉพาะ `documentId` **ของทีมคุณเอง** id ของทีมอื่น → **404** เหมือน id ที่ไม่มีอยู่จริง
และ **เลขหน้าจะถูกคำนวณใหม่ตามเอกสารที่รวมแล้ว** — สัญญา 2 หน้า + สำเนา 2 หน้า → ท้ายหน้า 3 เขียนว่า "หน้า 3 จาก 4"

### 8) เมื่อข้อมูลผิดสัญญา — ถูกจับที่ชายแดนก่อนสร้าง PDF

```bash
curl -s -X POST "$BASE/v1/generate"   -H "X-Api-Key: $THINKER_DOCS_API_KEY" -H 'Content-Type: application/json'   -d '{
    "templateId": "'"$TID"'", "version": 2, "mode": "sync",
    "data": { "age": "สี่สิบ" }
  }'
```

```json
400 Bad Request
{
  "error": "data does not match the template field contract",
  "code": "invalid_data",
  "fields": [
    { "field": "age",     "reason": "expected a number" },
    { "field": "case_id", "reason": "required" }
  ]
}
```

### 9) พิสูจน์ว่าไฟล์ที่คุณถืออยู่คือไฟล์ที่ระบบออกให้

```bash
shasum -a 256 out.pdf
# e4aec0e72ba031a731983d771b1714882eeb9dcbdb900bcf042a0c42087d3615
# ต้องตรงกับ sha256 ที่ API ตอบกลับมา — ไม่ต้องเก็บ PII ใดๆ เพื่อพิสูจน์เรื่องนี้
```
