Integrate DocIntel API

Developer Guide · ฝัง API สแกน + สกัดข้อมูลเอกสารไทยในระบบของคุณ

Table of contents

  1. 0 · ภาพรวม (Overview)
  2. 1 · Authentication
  3. 2 · Preflight — ตรวจก่อน
  4. 3 · Extract — สแกนเต็ม
  5. 4 · รับผลลัพธ์ (Jobs)
  6. 5 · Webhooks
  7. 6 · Compare / Writer / Delete
  8. 7 · Error handling
  9. 8 · Tools
  10. 9 · 📦 Response Reference ทุก doc_type (หน้าแยก · copy JSON ได้)
  11. Δ · อัปเดตล่าสุด 6/9/2569 — สิ่งที่เปลี่ยนในคำตอบ

0ภาพรวม — API นี้ทำอะไร

DocIntel รับไฟล์เอกสาร (PDF/รูป) → OCR ภาษาไทย → สกัดข้อมูลเป็น JSON (เช่น เลขบัญชี, กำไร/ขาดทุน, วันหมดอายุบัตร) พร้อมตรวจความถูกต้องและปิดบัง PII. ออกแบบสำหรับเอกสารการเงิน/ราชการไทยโดยเฉพาะ.

Flow มาตรฐาน (2 ขั้น แยกกัน)

User อัพโหลดรูป → 1. Preflight
ตรวจคุณภาพ + ประเภท (~0.2s)
→ ระบบคุณเก็บรูป → 2. Extract
OCR เต็ม (คิว async)
→ Poll / Webhook รับผล
ทำไมแยก 2 ขั้น? Preflight เร็ว (ไม่ OCR) ใช้กรองรูปเสีย/ผิดประเภท ก่อนเก็บ — ไม่เปลือง OCR. Extract หนักกว่า (OCR) จึงเข้าคิวแยก. สองเส้นนี้ อิสระต่อกัน คุณเรียกเมื่อไรก็ได้ ไม่ต้องเรียกต่อกัน.

ประเภทเอกสารที่รองรับ (14)

📦 field ทั้งหมดของ response แต่ละประเภท (พร้อม % การปรากฏ + ปุ่ม copy JSON สำหรับ gen model): /schemas
doc_typeเอกสารdoc_typeเอกสาร
thai_id_cardบัตรประชาชนfull_fsงบการเงินเต็ม
passportหนังสือเดินทาง5yr_summaryงบย้อนหลัง 5 ปี
bookbankสมุดบัญชีธนาคารtrade_regทะเบียนการค้า/พาณิชย์
pnd50ภ.ง.ด.50affidavitหนังสือรับรองบริษัท
boj5บ.อ.จ.5 (บัญชีผู้ถือหุ้น)power_of_attorneyหนังสือมอบอำนาจ
pp30_summaryภ.พ.30simple_ledgerบัญชีรายรับรายจ่าย
income_expงบรายได้-ค่าใช้จ่ายcommunityเอกสารกลุ่ม/ชุมชน

ไม่รู้ประเภท? ไม่ต้องส่ง — ระบบจับเอง. ถ้าจับไม่ได้จะได้ unknown.

พื้นฐาน

Base URLhttps://docintel-demo.kiatdamrong.shop
AuthBearer key ต่อ tenant · ข้อมูลแยกกันต่อ tenant สมบูรณ์
Preflightsync — ตอบทันที ~0.2s
Extractasync — คืน job_id แล้ว poll / รับ webhook

1Authentication

ทุก request (ยกเว้น /api/v1/_meta) ต้องมี header Authorization: Bearer dci_live_xxxxx · key รับจากผู้ดูแลระบบ.

เรื่องรายละเอียด
Key formatdci_live_ ตามด้วยสตริงสุ่ม · ผูกกับ 1 tenant
Scopekey จำกัดสิทธิ์ได้ (extract · jobs · applicants · meta · cost · webhooks) · admin/MCP แยกต่างหาก
Rate limit60 req/นาที ต่อ IP (burst 60) สำหรับ /api/v1/* · + โควตาต่อชั่วโมงต่อ tenant
เกิน limitได้ 429 (rate) หรือ 503 preflight_busy (โหลดพุ่ง) → retry
CORSเปิดทุก origin · fetch จาก browser ตรงได้ (Bearer header ไม่ใช่ cookie)

ทดสอบ key ใช้งานได้ไหม

# เมตาดาต้า API (ไม่ต้อง auth)
curl https://docintel-demo.kiatdamrong.shop/api/v1/_meta

# ตรวจว่า key ใช้ได้ (ควรได้ 200)
curl https://docintel-demo.kiatdamrong.shop/api/v1/jobs \
  -H "Authorization: Bearer dci_live_xxxxx"
const API = 'https://docintel-demo.kiatdamrong.shop';
const KEY = 'dci_live_xxxxx';
const H = { 'Authorization': `Bearer ${KEY}` };

const jobs = await fetch(`${API}/api/v1/jobs`, { headers: H }).then(r => r.json());
console.log(jobs);
import requests
API = "https://docintel-demo.kiatdamrong.shop"
H = {"Authorization": "Bearer dci_live_xxxxx"}
print(requests.get(f"{API}/api/v1/jobs", headers=H).json())

2Preflight POST /api/v1/preflight

ตรวจ คุณภาพรูป + (ถ้าต้องการ) ยืนยันประเภทเอกสาร โดย ไม่ต้อง OCR — ตอบ sync ~0.2s. ใช้กรองรูปก่อนเก็บ/ก่อนส่ง Extract.

Request (multipart/form-data)

fieldtyperequiredรายละเอียด
filefilerequiredPDF / PNG / JPG / JPEG / WebP · สูงสุด 30 MB
expected_doc_typestringoptionalประเภทที่คุณคาดว่าจะเป็น (จากตาราง 14 ประเภท). ถ้าส่งมา ระบบจะยืนยันว่าตรงไหม (doc_type_match) — แม่นกว่าปล่อยให้เดาเอง
check_qualitybool= trueตรวจ ความคม/สว่าง/คอนทราสต์
check_typebool= trueตรวจประเภทเอกสาร

ตัวอย่าง — แนะนำให้ส่ง expected_doc_type

curl -X POST https://docintel-demo.kiatdamrong.shop/api/v1/preflight \
  -H "Authorization: Bearer dci_live_xxxxx" \
  -F "[email protected]" \
  -F "expected_doc_type=boj5"
const fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('expected_doc_type', 'boj5');

const r = await fetch(`${API}/api/v1/preflight`, {
  method: 'POST', headers: H, body: fd
}).then(r => r.json());

if (r.next_action === 'proceed') { /* เก็บรูป */ }
else if (r.next_action === 'review') { /* เตือน: ดูเหมือน r.detected */ }
else { /* retry_with_better_image */ }
r = requests.post(f"{API}/api/v1/preflight", headers=H,
    files={"file": open("slip.jpg", "rb")},
    data={"expected_doc_type": "boj5"}).json()
print(r["next_action"], r["detected"], r["doc_type_match"])

Response

fieldtypeความหมาย
passedboolรูป ใช้ได้ไหม (คุณภาพ + อ่านออก). ไม่ผูกกับการตรงประเภท
quality_okboolผ่านเกณฑ์คุณภาพไหม
methodstringvision = ระบุประเภทได้เร็ว · needs_ocr = ต้องให้ Extract ตัดสินประเภท (รูปยังใช้ได้)
detectedstring|nullประเภทที่ระบบเห็น (null เมื่อ needs_ocr)
deep_checkform·boolพารามิเตอร์ตอนเรียก (default false) — ถ้าภาพชี้ขาดประเภทไม่ได้ ให้ระบบอ่านข้อความเฉพาะหน้าแรกแล้วจำแนกให้เลย: ได้ method: "ocr_page1" + detected/doc_type_match เมื่อความมั่นใจ ≥ 0.5 (วัดจริง: ครอบ ~79% แม่น ~96%) · ต่ำกว่าเกณฑ์จะไม่ฟันธง (รายงานใน reasons แทน) · แลกกับเวลา ~10-20 วิ+ ขึ้นกับเครื่อง — ใช้เฉพาะตอนจำเป็น อย่ายิงถี่ · ผล mismatch เป็นคำเตือนให้ review ไม่ใช่การปฏิเสธ
findingsarrayรายการปัญหาที่ตรวจพบแบบมีโครงสร้าง — แต่ละรายการ: code / severity (error=ภาพใช้ไม่ได้ · warning=เตือนให้ตรวจทาน) / message (ไทย พร้อมวิธีแก้) / detail · รหัสที่มี: image_blurry image_too_dark image_overexposed image_low_contrast image_too_small out_of_scope_hint type_mismatch · ว่าง = ไม่พบปัญหา · branch จาก code ได้เลย ไม่ต้อง parse ข้อความใน reasons
scopeobjectมีเฉพาะเมื่อเอกสารไม่ใกล้เคียงประเภทใดที่ระบบรองรับ (scope_warning: true + nearest_type / distance) — คำเตือน ไม่ใช่คำตัดสิน next_action ยังเป็น proceed ตามเดิม · ถ้าฝืนส่งต่อมีโอกาสสูงที่จะได้ผล unknown
doc_type_matchbool|nulldetected ตรงกับ expected_doc_type ไหม — null ทั้งเมื่อไม่ส่ง expected และเมื่อ method=needs_ocr (ยังไม่รู้ประเภท) · อย่าตีความ null ว่า "ตรง" — ประเภทจริงยืนยันจากผล /extract
next_actionstringสิ่งที่ระบบแนะนำ — ดูตารางล่าง
qualityobject{ sharpness, brightness, contrast, width, height, issues[] }
reasonsarrayคำอธิบายเหตุผล (เช่น doc_type_mismatch, low_sharpness)

next_action — Preflight รายงาน, ระบบคุณตัดสินใจ

next_actionแปลว่าควรทำ
proceedรูปใช้ได้ · ประเภทยืนยันแล้ว/รอ OCRเก็บรูป → ส่ง Extract เมื่อพร้อม
reviewรูปใช้ได้ แต่ vision เห็น detected ≠ ที่คุณคาดเตือน user ("ดูเหมือน {detected}") — รับ / ให้อัพใหม่ / ส่ง Extract ให้ OCR ตัดสิน
retry_with_better_imageคุณภาพต่ำ (เบลอ/มืด/สว่างเกิน)ให้ user ถ่าย/อัพใหม่
Preflight จะไม่ block เอกสารเอง — mismatch = คำเตือน (review) ไม่ใช่ reject. ระบบคุณตัดสินใจ. ถ้าอยากให้ OCR เป็นคนตัดสินประเภทชี้ขาด = เรียก Extract (ขั้น 3).

3Extract POST /api/v1/extract

สแกน + สกัดข้อมูลเต็ม (OCR). เป็น async — คืน job_id ทันที (สถานะ 202) แล้วประมวลผลเบื้องหลัง. ดูผลด้วยขั้น 4.

Request (multipart/form-data)

fieldtyperequiredรายละเอียด
filefilerequiredPDF / PNG / JPG / JPEG / WebP · สูงสุด 30 MB
external_idstringrequiredรหัสอ้างอิงฝั่งคุณ · 1-128 ตัว · [A-Za-z0-9_-] · ห้ามซ้ำใน tenant ภายใน 90 วัน (ซ้ำ = 409)
applicant_idstringrequiredรหัสบุคคล · opaque · 1-256 ตัว · ใช้ hash/UUID ไม่ใช่เลขบัตรจริง
case_idstringoptionalจัดกลุ่มเคส · รูปแบบเดียวกับ external_id
applicant_namestringoptionalชื่อผู้สมัคร (ถ้ามี)
tagsjsonoptionalarray · ["loan","urgent"] · สูงสุด 20 · kebab-case
metadatajsonoptionalobject อิสระ · สูงสุด 4 KB
doc_type_hintstringoptionalบอกใบ้ประเภท (ไม่บังคับ · ระบบจับเองได้)
preprocessbool= trueปรับภาพก่อน OCR
enable_ensemblebool= falseใช้หลายโมเดลรวมผล (แม่นขึ้น ช้าลง)
enable_mrzbool= falseอ่านแถบ MRZ (passport)
enable_vision_checkbool= falseตรวจซ้ำด้วย vision model — ความมั่นใจสูงขึ้น (ช้าลงเล็กน้อย)
backendstring= autoเครื่อง OCR: auto (ใช้ในเครื่องก่อน · cloud สำรอง) · local · cloud

Headers พิเศษ

Idempotency-Keyuuidกันส่งซ้ำ 24 ชม. — ส่งซ้ำด้วย key เดิม = คืน job เดิม
X-Force-Reextracttrueข้าม cache + ข้ามกฎ external_id ซ้ำ (บังคับสแกนใหม่)
curl -X POST https://docintel-demo.kiatdamrong.shop/api/v1/extract \
  -H "Authorization: Bearer dci_live_xxxxx" \
  -F "[email protected]" \
  -F "external_id=loan-2024-00123" \
  -F "applicant_id=usr_8f3a1c" \
  -F 'tags=["loan","urgent"]'
# → { "job_id": "...", "status": "queued", "poll_url": "/api/v1/jobs/..." }
const fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('external_id', 'loan-2024-00123');
fd.append('applicant_id', 'usr_8f3a1c');

const { job_id, poll_url } = await fetch(`${API}/api/v1/extract`, {
  method: 'POST', headers: H, body: fd
}).then(r => r.json());
r = requests.post(f"{API}/api/v1/extract", headers=H,
    files={"file": open("statement.pdf", "rb")},
    data={"external_id": "loan-2024-00123",
          "applicant_id": "usr_8f3a1c"}).json()
job_id = r["job_id"]
409 external_id_conflict = external_id นี้ถูกใช้ใน 90 วันแล้ว. ใช้ค่าใหม่ หรือส่ง header X-Force-Reextract: true เพื่อสแกนซ้ำ.

4รับผลลัพธ์ GET /api/v1/jobs/{job_id}

Extract คืน job_id — poll endpoint นี้จนสถานะเป็น done.

สถานะงาน

queued→ processing→ done | failed / error

โครงสร้าง response (เมื่อ done)

⚡ ใช้เส้นเบาสำหรับระบบจริง: GET /api/v1/jobs/{job_id}/extraction คืนเฉพาะ extraction · _status · validation (+ job_id, external_id, doc_type, extracted_at, tier_used) ขนาด ~2 KB · ส่วน GET /api/v1/jobs/{job_id} เต็มๆ มี pages[] พร้อมรูปหน้าเอกสารแบบ base64 (~90 KB) เหมาะกับหน้า review เท่านั้น · หรือใช้ ?fields=meta,extraction,validation เพื่อตัด pages ทิ้ง
📦 Reference ครบทุก field ทุกประเภทเอกสาร — field ทุกตัวที่เคยปรากฏจริง พร้อม % การปรากฏ + ปุ่ม copy JSON เอาไป gen model ฝั่งคุณได้เลย: /schemas
สำคัญ: ฟิลด์ผลลัพธ์อยู่ระดับบนสุด (top-level) ของ object — ไม่ได้อยู่ใต้ .result.
statusstringqueued / processing / done / failed / error
doc_typestringประเภทที่จับได้ (ชี้ขาดโดย OCR)
classify_confidencefloatความมั่นใจในการจับประเภท (0-1)
(fields สกัด)variousข้อมูลที่สกัด แยกตาม doc_type อยู่ top-level (เช่น account_no, net_profit, expiry_date)
validation_passed / totalintผ่านการตรวจสอบกี่ข้อ จากทั้งหมด
errorstring|nullข้อความ error (เมื่อ status=failed)
reviewobject|nullผลตรวจทานล่าสุดจากฝั่งคุณ (ดูหัวข้อ "ส่งผลตรวจกลับ" ด้านล่าง) — null ถ้ายังไม่เคยส่ง

Poll จนเสร็จ

curl https://docintel-demo.kiatdamrong.shop/api/v1/jobs/JOB_ID \
  -H "Authorization: Bearer dci_live_xxxxx"
async function waitJob(id) {
  while (true) {
    const j = await fetch(`${API}/api/v1/jobs/${id}`, { headers: H }).then(r => r.json());
    if (j.status === 'done') return j;
    if (j.status === 'failed' || j.status === 'error') throw j;
    await new Promise(res => setTimeout(res, 2000)); // รอ 2s
  }
}
import time
def wait_job(job_id):
    while True:
        j = requests.get(f"{API}/api/v1/jobs/{job_id}", headers=H).json()
        if j["status"] == "done": return j
        if j["status"] in ("failed", "error"): raise Exception(j)
        time.sleep(2)

ไฟล์เดียวที่มีหลายเอกสาร (composite)

~10% ของไฟล์ที่อัพโหลดเป็นชุดเอกสาร — เช่น ภ.ง.ด.50 + งบการเงิน + สมุดบัญชี รวมในไฟล์เดียว ซึ่งเป็นรูปแบบปกติของชุดยื่นกู้ ระบบตรวจเจอและสกัดแยกทีละส่วนให้อยู่แล้ว
classification.is_compositeboolไฟล์นี้มีมากกว่าหนึ่งเอกสาร
classification.composite_sections[]arrayแต่ละส่วนมี doc_type · start_page / end_page · และ extraction ของส่วนนั้นเอง
⚠️ ต้องอ่านตรงนี้ด้วย — extraction ระดับบนสุดคือของส่วนแรกเท่านั้น ถ้าอ่านแค่ตัวนั้น ส่วนที่เหลือจะหายไปเงียบ ๆ (ในคลังตัวอย่างคือ 1,141 ส่วน · 2,119 หน้า · ในนั้นเป็นงบการเงิน 425 และ ภ.ง.ด.50 268)
sections = job["classification"].get("composite_sections") or []
for s in sections:
    print(s["doc_type"], s["start_page"], "-", s["end_page"], s["extraction"])

อยากได้เฉพาะฟิลด์ที่สกัด → GET /api/v1/jobs/{id}/extraction · รูปหน้าเอกสาร → /jobs/{id}/pages/{n}/image · ข้อความ OCR → /pages/{n}/text

เมื่อระบบไม่มั่นใจ — สัญญาณที่ต้องอ่านก่อนนำค่าไปใช้

ระบบไม่ปล่อยผ่านเงียบ ๆ — เมื่ออ่านเอกสารแล้วไม่มั่นใจ ผลลัพธ์จะมีสัญญาณแจ้งไว้เสมอ ระบบของคุณควรอ่านสัญญาณเหล่านี้ แล้วส่งงานเข้ากระบวนการตรวจทานโดยเจ้าหน้าที่ (human review) ก่อนใช้ค่า
doc_type = "unknown"stringระบบจำแนกประเภทเอกสารไม่ได้ (เลือกที่จะไม่เดา) — ให้คนระบุประเภทเอง
classify_confidence ต่ำfloatความมั่นใจในการจำแนกต่ำ — response จะมี classification._warning อธิบายเพิ่ม
extraction.address_partsobjectที่อยู่แยกส่วนให้แล้ว ไม่ต้องตัด string เอง: street_address (ที่อยู่ส่วนหน้ารวม ตัดคำฟอร์ม เช่น "ตั้งอยู่เลขที่/สำนักงานแห่งใหญ่" ให้แล้ว) / house_no / moo / village / soi / road / subdistrict / district / province / postal_code · province_source บอกที่มา — "text" อ่านจากเอกสาร · "postal_code" อนุมานจากรหัสไปรษณีย์ · มีทุกประเภทเอกสารที่พบที่อยู่ (ฟิลด์ address เดิมคงอยู่ตามเดิม)
financials.accounting_net_profitnumberภ.ง.ด.50 มีกำไรสุทธิสองตัว และได้ทั้งคู่: net_profit = กำไรสุทธิที่ต้องเสียภาษี (รายการที่ 2 ข้อ 21 · ฐานภาษี) · accounting_net_profit = กำไรสุทธิตามบัญชี (ข้อ 9 · ผลประกอบการตามงบ) · แต่ละตัวมี _..._source บอกบรรทัดที่อ่านมา
extraction._rejectedobjectค่าที่ระบบกักไว้แทนที่จะส่งเป็นคำตอบ — ฟิลด์หลักจะเป็น null และค่าที่อ่านได้อยู่ที่นี่พร้อมเหตุผล: not_in_document (ตัวเลขไม่ปรากฏในเอกสาร = อ่านมั่ว) · registration_equals_request (เลขทะเบียนไปซ้ำเลขคำขอ) · mod11_failed (เฉพาะกรณีไม่มีข้อความ OCR ให้ตรวจ) · เชื่อ null + ตรวจเองได้จากตรงนี้ · ตั้งแต่ 6/9/2569 เลขที่พิมพ์อยู่ในเอกสารจะไม่ถูกกักอีกต่อไป แม้ไม่ครบ 13 หลัก — ดูหัวข้อ Δ
extraction._warningsobjectคำเตือนต่อฟิลด์ (key = ชื่อฟิลด์) พร้อม reason และ note ภาษาไทยที่แสดงผู้ใช้ได้ทันที · เหตุผลหลัก: digits_over_13 / digits_under_13 (เลขที่ต้องเป็น 13 หลักแต่พิมพ์มาเกิน/ขาด — ส่งตามที่พิมพ์ พร้อม over/under กี่หลัก) · checksum_failed (13 หลักแต่เช็คหลักไม่ผ่าน) · digit_recovered (ระบบเติมหลักที่หายด้วยเช็คหลัก — บอกตำแหน่ง ตัวเลข และ value_as_read) · other_copies_off (ใช้สำเนาที่ครบ แต่จุดอื่นในเอกสารอ่านได้ขาด/เกิน) · เมื่อมี warning ใด ๆ review_reasons จะมี extraction_warnings
needs_review · review_reasonsbool · arrayอยู่ระดับบนสุดของทุกงานที่ done (เดิมมีเฉพาะใน webhook) — เหตุผล: extraction_warnings · fields_withheld · validation_failed · low_classify_confidence · doc_type_unknown · suspected_hallucination · ocr_runaway (OCR อ่านวนซ้ำ) · ocr_sparse (อ่านได้น้อยผิดปกติ) · ocr_engine_fallback / ocr_cloud_fallback (หน้าใดหน้าหนึ่งถูกอ่านด้วยเอนจินสำรอง คุณภาพอาจต่าง) · ถ้า true ให้ส่งเข้า human review
extraction._fillobjectฟิลด์ที่ว่างจากตัวสกัดหลักแล้วถูกเติมโดย tier เสริม: filled[] (เติมสำเร็จ) · empties[] (ที่ถามไป) · dropped_ungrounded[] (คำตอบที่ไม่มีในข้อความ OCR — ทิ้ง) · skipped[] (ฟิลด์ที่ไม่ยอมให้เติม) · ค่าที่เติมทุกตัวต้องปรากฏในเอกสารเท่านั้น
pages[].ocr_retry · ocr_engine_fallback · text_sourceobject · object · stringต่อหน้า: ocr_retry = หน้านี้ถูกอ่านซ้ำเพราะรอบแรกวนซ้ำ (method, first_chars, second_chars, still_runaway) · ocr_engine_fallback = อ่านด้วยเอนจินสำรอง (from, reason) · text_source = "ocr_structure_retry" = ข้อความมาจากการอ่านซ้ำ
extraction.extraction_statusstring"suspected_hallucination" = เคสสงสัยระดับสูง — ต้องให้คนตรวจก่อนใช้ค่าเสมอ
validation (รายข้อ)arrayผลตรวจตามกฎรายข้อ {check, passed} — ถ้า validation_passed < validation_total ให้ดูว่าข้อไหนไม่ผ่าน (ขอผ่าน ?fields=validation)

แนวทางตรวจทานฝั่งคุณ: ดึงรูปหน้าเอกสาร GET /api/v1/jobs/{id}/pages/{n}/image มาแสดงคู่กับค่าที่สกัดได้ ให้เจ้าหน้าที่เทียบกับต้นฉบับ แล้วแจ้งผลกลับด้วย endpoint ด้านล่าง — ทั้งสองฝั่งจะได้เห็นภาพตรงกันว่าเอกสารใดผ่านการยืนยันแล้ว

ส่งผลตรวจกลับ POST /api/v1/jobs/{job_id}/review

ปิดลูปการตรวจทาน: เจ้าหน้าที่ของคุณตรวจแล้ว แจ้งผลกลับมาให้ระบบบันทึกไว้ ผลล่าสุดจะแสดงใน GET /jobs/{id} field review (หรือขอเฉพาะส่วนนี้ด้วย ?fields=review)

verifiedbool · requiredtrue = ผลสกัดถูกต้องตามต้นฉบับ · false = พบข้อผิดพลาด
corrected_doc_typestring · optionalประเภทที่ถูกต้อง กรณีระบบจำแนกผิด (ต้องเป็นหนึ่งใน 14 ประเภท หรือ other/unknown)
corrected_fieldsobject · optionalค่าที่ถูกต้องรายฟิลด์ เช่น {"account_no": "123-4-56789-0"} (สูงสุด 50 คีย์)
notestring · optionalบันทึกเพิ่มเติมจากผู้ตรวจ (ไม่เกิน 1,000 ตัวอักษร)
curl -X POST https://docintel-demo.kiatdamrong.shop/api/v1/jobs/JOB_ID/review   -H "Authorization: Bearer dci_live_xxxxx"   -H "Content-Type: application/json"   -d '{"verified": false, "corrected_doc_type": "bookbank",
       "corrected_fields": {"account_no": "123-4-56789-0"},
       "note": "เจ้าหน้าที่ตรวจกับต้นฉบับแล้ว"}'
await fetch(`${API}/api/v1/jobs/${jobId}/review`, {
  method: 'POST',
  headers: { ...H, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    verified: false,
    corrected_doc_type: 'bookbank',
    corrected_fields: { account_no: '123-4-56789-0' },
    note: 'เจ้าหน้าที่ตรวจกับต้นฉบับแล้ว',
  }),
});
requests.post(f"{API}/api/v1/jobs/{job_id}/review", headers=H, json={
    "verified": False,
    "corrected_doc_type": "bookbank",
    "corrected_fields": {"account_no": "123-4-56789-0"},
    "note": "เจ้าหน้าที่ตรวจกับต้นฉบับแล้ว",
})

ส่งซ้ำได้ (ระบบเก็บเป็นประวัติ · แสดงรายการล่าสุด) · ใช้ได้เฉพาะงานสถานะ done (ไม่เช่นนั้นได้ 409) · การส่งผลตรวจถูกบันทึกใน audit log ตามแนวทาง PDPA เช่นเดียวกับการเข้าถึงข้อมูล

5Webhooks — รับผลแบบไม่ต้อง poll

ลงทะเบียน URL (HTTPS) เพื่อให้ระบบยิงกลับเมื่องานเสร็จ — แทนการ poll.

สร้าง POST /api/v1/webhooks

curl -X POST https://docintel-demo.kiatdamrong.shop/api/v1/webhooks \
  -H "Authorization: Bearer dci_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yourapp.com/hook","events":["extract.done","extract.failed"]}'

Events ที่รองรับ

extract.doneงาน extract เสร็จสมบูรณ์
extract.failedงาน extract ล้มเหลว
key.revokedkey ถูกยกเลิก

Headers ที่ระบบส่งมา

X-DocIntel-Event-Typeเช่น extract.done
X-DocIntel-Event-Idid เหตุการณ์ (กันซ้ำ · idempotent)
X-DocIntel-Signaturet=<ts>,v1=<hmac> (Stripe-style) — verify ก่อนเชื่อ

Payload (body ที่ส่งมา)

{
  "event_type": "extract.done",
  "job_id": "da78be19-b693-...",
  "tenant": "your-tenant",
  "status": "done",
  "filename": "statement.pdf",
  "applicant_id": "usr_8f3a1c",
  "duration_s": 12.34,
  "completed_at": 1720000000,
  "doc_type": "boj5",          // เฉพาะ extract.done
  "classify_confidence": 0.94,
  "needs_review": false,        // true = มีสัญญาณไม่มั่นใจ → ส่งเข้าคิวคนตรวจ
  "review_reasons": []           // เหตุผล (ว่าง = ไม่มีสัญญาณ)
  // extract.failed จะมี "error": "..." แทน
}

review_reasons ที่เป็นไปได้: doc_type_unknown · low_classify_confidence · suspected_hallucination · extraction_warnings · validation_failed · fields_withheld (เพิ่ม 26/8 — ระบบกักค่าที่อ่านไม่ได้จริงไว้ใน extraction._rejected แทนที่จะเดา · เปิดดูรายชื่อ field และเหตุผลได้ในนั้น) — ความหมายแต่ละตัวตรงกับหัวข้อ "เมื่อระบบไม่มั่นใจ" ในข้อ 4 ระบบคุณใช้ needs_review ตัดสินใจได้ทันทีจาก webhook โดยไม่ต้องดึงผลเต็มก่อน

Payload เป็นการแจ้งเตือน — ไม่มีข้อมูลที่สกัดเต็ม. เมื่อได้ extract.done แล้วค่อยเรียก GET /api/v1/jobs/{job_id} เพื่อดึงผลเต็ม.

Verify signature

import hmac, hashlib

def verify(secret, sig_header, raw_body):
    p = dict(x.split("=", 1) for x in sig_header.split(","))
    signed = f'{p["t"]}.{raw_body}'.encode()
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, p["v1"])
# raw_body = ตัว body ดิบ (ห้าม re-serialize ก่อน verify)
const crypto = require('crypto');
function verify(secret, sigHeader, rawBody) {
  const p = Object.fromEntries(sigHeader.split(',').map(x => x.split('=')));
  const signed = `${p.t}.${rawBody}`;
  const exp = crypto.createHmac('sha256', secret).update(signed).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(p.v1));
}

หมุน secret: /webhooks/{id}/rotate-secret · จัดการ: GET /webhooks · /webhooks/{id}/disable · /delete

6เพิ่มเติม — Compare · Writer · Delete

เปรียบเทียบงบ (YoY) POST /api/v1/compare · scope: jobs

ส่งหลาย job_id ของงบต่างปี → ได้ผลต่างทีละฟิลด์ + เทียบ trend/อัตราส่วน. หรือเทียบเอกสารของ applicant คนเดียวผ่าน /applicants/{id}/compare.

AI Writer POST /api/v1/improve-objective · scope: writer

ตัวช่วย เจน/เรียบเรียงข้อความภาษาไทย ให้เป็นทางการขึ้น ตามอินพุตที่ frontend กำหนด. ไม่เกี่ยวกับเอกสารที่สแกน — ไม่ได้ดึงข้อมูลจาก extract. ระบบจะไม่แต่งข้อเท็จจริงใหม่ (ปรับภาษา/โครงสร้างเท่านั้น).

Request (JSON body):

fieldtyperequiredรายละเอียด
draftstringrequiredข้อความตั้งต้นที่อยากให้ปรับ (ประโยคที่ต้องการควบคุม) · 5–2000 ตัว
keywordsstring[]optionalคำสำคัญที่อยากให้เน้น · ≤ 20 คำ · คำละไม่เกิน 100 ตัวอักษร (เกินจะได้ 422) · ช่วยให้ตรงประเด็น (ไม่ยัดคำแบบแต่งข้อเท็จจริง)
project_contextstringoptionalข้อมูลเสริม/บริบท เพื่อให้ข้อความที่เจนออกมาตรงเนื้อหา · ≤ 2000 ตัว
external_idstringoptionalecho กลับใน response เพื่อ tracking
curl -X POST https://docintel-demo.kiatdamrong.shop/api/v1/improve-objective \
  -H "Authorization: Bearer dci_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "draft": "โครงการนี้จะช่วยเกษตรกรในชุมชนให้ดีขึ้น",
    "keywords": ["ความยั่งยืน","รายได้","เทคโนโลยี"],
    "project_context": "ทุนพัฒนาชุมชนเกษตร"
  }'
# → { "improved": "...", "original_length": .., "improved_length": .., "_model": ".." }
r = requests.post(f"{API}/api/v1/improve-objective", headers=H, json={
    "draft": "โครงการนี้จะช่วยเกษตรกรในชุมชนให้ดีขึ้น",
    "keywords": ["ความยั่งยืน", "รายได้", "เทคโนโลยี"],
}).json()
print(r["improved"])

Response: improved · original_length · improved_length · _model · _tokens · _duration_ms

เพิ่มใหม่ 25/8 — Writer อีก 2 หัวข้อ · request body / response / error เหมือน improve-objective ทุกฟิลด์ (คีย์ scope writer เดิมใช้ได้เลย) ต่างแค่ URL ตามช่องในฟอร์ม:

ช่องในฟอร์มendpointพฤติกรรมเฉพาะหัวข้อ
วัตถุประสงค์POST /api/v1/improve-objectiveตัวเดิม — ไม่เปลี่ยนแปลง
ผลที่คาดว่าจะได้รับPOST /api/v1/improve-expected-resultsข้อเสนอโครงการ · ถ้าร่างมีหลายผล ระบบจัดเป็นข้อ 1. 2. 3. ให้เอง · ไม่เติม KPI/ตัวเลขที่ผู้ใช้ไม่ได้เขียน
ที่มา/สาเหตุของปัญหาPOST /api/v1/improve-problem-backgroundเบิกจ่าย · เรียบเรียงลำดับ ปัญหา → ผลกระทบ → ความต้องการ จากเฉพาะสิ่งที่ผู้ใช้เขียน

response ของ 2 เส้นใหม่เพิ่มฟิลด์ field_type (expected_results / problem_background) บอกว่าหัวข้อไหนตอบ · ตัวอย่างผลจริง + รายละเอียดเต็มอยู่ในคู่มือ AI Writer หัวข้อ 8

PDPA · ลบข้อมูล DELETE /api/v1/jobs/{job_id}

ลบงานและข้อมูลที่เกี่ยวข้องถาวร (ตามสิทธิ PDPA ของเจ้าของข้อมูล).

ค้นหา / ไทม์ไลน์ applicant

/applicants/search · /applicants/{id}/timeline · /applicants/{id}/documents · ค่าใช้จ่าย /cost-summary

Δอัปเดต 6/9/2569 — สิ่งที่เปลี่ยนในคำตอบ

เส้นทาง วิธีเรียก และการยืนยันตัวตนไม่เปลี่ยน — ทุกการเปลี่ยนแปลงอยู่ในคำตอบและเป็นแบบ "เพิ่มเข้า" (key เดิมอยู่ครบ) · เอกสารฉบับเต็ม: /changes · Reference ทุก field regenerate แล้วที่ /schemas

1 · เลขที่ต้องเป็น 13 หลัก จะส่งตามที่พิมพ์ในเอกสาร พร้อมคำเตือน (นโยบายใหม่)

ใช้กับ trade_reg.registration_number · full_fs.company.tax_id · pnd50.tax_id · boj5.company_tid — ระบบไม่ตัดสินแทนว่าเลขถูกหรือผิด แต่บอกว่าอ่านได้กี่หลัก ขาดหรือเกิน แล้วให้ผู้ใช้ตรวจกับต้นทาง

13 หลัก ผ่านเช็คหลักstringส่งค่า · ไม่มี warning
13 หลัก เช็คหลักไม่ผ่าน (พิมพ์อยู่ในเอกสาร)stringส่งค่า (เดิมเป็น null) · _warnings.<field>.reason = checksum_failed · <field>_checksum_pass = false
14 หลัก (พิมพ์เกิน)stringส่งค่า 14 หลัก · digits_over_13 + over: 1
12 หลัก (พิมพ์ขาด / อ่านตก) ไม่มีสำเนาครบในเอกสารstringส่งค่า 12 หลัก · digits_under_13 + under: 1
12 หลัก แต่ระบบเติมหลักที่หายด้วยเช็คหลัก (ภ.ง.ด.50 · บอจ.5)stringส่งค่า 13 หลักที่เติมแล้ว · digit_recovered + position digit value_as_read
ใช้สำเนาที่ครบในเอกสาร แต่จุดอื่นอ่านได้ขาด/เกินstringส่งค่า 13 หลัก · other_copies_off + variants[]
เลขที่ไม่มีในข้อความเลยnullกักไว้ใน _rejected reason not_in_document
ผลต่อโค้ดฝั่งคุณ: ถ้ามี validation ว่า "ต้อง 13 หลัก" ให้อ่าน _warnings.<field> ประกอบ · note ทุกตัวเป็นภาษาไทยพร้อมแสดงผู้ใช้

2 · key ใหม่ในคำตอบ

needs_review / review_reasons ใน GET (เดิมเฉพาะ webhook) · extraction._fill · pages[].ocr_retry · pages[].ocr_engine_fallback · pages[].text_source = "ocr_structure_retry" · เหตุผล review ใหม่ ocr_runaway ocr_sparse ocr_engine_fallback ocr_cloud_fallback extraction_warnings fields_withheld — รายละเอียดในหัวข้อ 4 "สัญญาณที่ต้องอ่าน"

3 · ชื่อเอนจิน

model_used · pages[].ocr_model · extraction._model เป็นชื่อกลาง (เช่น "AI Gateway OCR") — อย่าผูกโค้ดกับค่าเหล่านี้

4 · พฤติกรรมที่เปลี่ยน

7Error handling

รูปแบบเดียวทุก error (ตั้งแต่ 20/8/2026) — ไม่ว่าจะเป็น auth, validation, ข้อมูลซ้ำ หรือ server error ทุกตัวมี object error หน้าตาเดียวกัน เสมอ · ฟิลด์ detail เดิมยังอยู่ (เผื่อ client เก่า) แต่ ให้ผูก model กับ error อย่างเดียว
{
  "error": {
    "code":       "external_id_conflict",   // token คงที่ snake_case — ใช้ switch/case
    "message":    "external_id 'x' already used within 90-day window",
    "status":     409,                      // = HTTP status
    "details":    { "existing_job_id": "...", "hint": "..." },   // หรือ null
    "request_id": "cid_4df968c8d21e"        // ส่งให้เราเวลาแจ้งปัญหา
  },
  "detail": ...                             // legacy · จะเลิกใช้ในอนาคต
}

422 validation จะมี details.fields[] บอกชื่อ field ที่ผิดทีละตัว: {"field": "applicant_id", "message": "Field required", "type": "missing"}

แคตตาล็อก error.code

codestatusหมายถึง
customer_key_required · not_a_customer_key · invalid_customer_key · key_expired · key_revoked401ปัญหา API key
forbidden403key ไม่มี scope / ข้าม tenant
not_found · webhook_not_found404ไม่พบ job / webhook
external_id_conflict409external_id ซ้ำใน 90 วัน · details.existing_job_id ชี้งานเดิม
webhook_not_found_or_version_mismatch · webhook_not_found_or_already_disabled409webhook ถูกแก้ไปแล้ว · ส่ง expected_version ล่าสุด
payload_too_large413ไฟล์เกิน 30 MB
unsupported_media_type415ชนิดไฟล์ไม่รองรับ
validation_error422body/form ไม่ผ่าน · ดู details.fields หรือ message
expected_version_required · url_required400ขาด field บังคับของ webhook API
per_key_rate_limit_exceeded · tenant_rate_limit_exceeded · tenant_concurrency_limit429เกินโควตา · รอตาม Retry-After
preflight_busy · memory_pressure · service_draining · audit_unavailable503ระบบไม่ว่างชั่วคราว · retry สั้นๆ
internal_error500ข้อผิดพลาดฝั่งเรา · แจ้ง request_id มาได้เลย

กติกา: code ที่เป็น token เฉพาะจะคงที่ตลอด (ผูก logic ได้) · ถ้าไม่มี token เฉพาะจะได้ code กลางตาม status (bad_request / unauthorized / not_found / conflict / validation_error / rate_limited / internal_error ฯลฯ) และ message อธิบายเป็นข้อความเสมอ

ตาราง HTTP status

Statusหมายถึงวิธีแก้
401ไม่มี/ผิด keyใส่ header Authorization: Bearer ... ให้ถูก
403key ไม่มีสิทธิ์ (scope) หรือข้าม tenantใช้ key ที่มี scope นั้น
409external_id ซ้ำใน 90 วันใช้ค่าใหม่ หรือ X-Force-Reextract: true
413ไฟล์ใหญ่เกิน 30 MBย่อ/บีบไฟล์
415ชนิดไฟล์ไม่รองรับใช้ PDF/PNG/JPG/JPEG/WebP
422body ไม่ผ่าน validation (เช่น external_id ผิดรูปแบบ)อ่าน detail — บอกฟิลด์ที่ผิด
429เกิน rate limitรอตาม Retry-After แล้วลองใหม่
503preflight_busy — โหลดพุ่งชั่วคราวretry สั้นๆ (ไม่ใช่ error ที่ request คุณ)
500server errorลองใหม่ · ถ้าซ้ำแจ้งผู้ดูแล

8Tools

Swagger (/docs) แสดง ทุก endpoint รวม admin — ลูกค้าใช้แค่เส้น /api/v1/* ที่อยู่ในหน้านี้. ที่เหลือเป็นของผู้ดูแลระบบ.
ไฟล์ตัวอย่างไว้ลองยิง — GET /api/v1/samples คืนรายชื่อประเภทที่มีไฟล์ตัวอย่าง และ GET /api/v1/sample/{doc_type} ดึงไฟล์นั้นมาได้เลย. ทั้งสองเส้นเปิดสาธารณะ ไม่ต้องใช้ API key — เอาไปลอง /preflight หรือ /extract ได้ทันทีโดยไม่ต้องเตรียมไฟล์เอง. เอกสารชุดนี้ผ่านการคัดกรองข้อมูลส่วนบุคคลแล้ว.