เอกสาร Open API
Ket Bill Open API v1 · ให้ระบบภายนอกและผู้ช่วย AI ทำงานแทนบริษัทของคุณ
base url
https://api.ketbill.com
/v1
REST over HTTPS · JSON (UTF-8)
auth
Bearer token
API key ขึ้นต้น kdoc_ หรือ OAuth 2.1 access token
rate limit
30 req/s
ต่อคีย์ API หรือต่อ access token · เกินแล้วได้ 429 พร้อม Retry-After
ระบบภายนอก n8n, ผู้ช่วย AI หรือ backend ของพาร์ตเนอร์ อ่าน สร้าง และแก้เอกสารบัญชี ลูกค้า และสินค้าแทนบริษัทหนึ่งได้ ทุกคำขอผูกกับบริษัทของ token ใบนั้นเสมอ ข้ามบริษัทไม่ได้ · สเปกฉบับเครื่องอ่าน (OpenAPI 3.0) ใช้ generate client หรือให้ AI agent อ่านทั้งชุด รายการ endpoint ในหน้านี้มาจากไฟล์เดียวกันนั้น
Read, create and update documents, clients and products on behalf of exactly one company.
เริ่มต้นใช้งาน
Create a key, verify it against /me, then post your first draft document.
สร้างคีย์ API ในแอป
เข้า ketbill.com → ตั้งค่า → การเชื่อมต่อ → API Keys แล้วกดสร้างคีย์ เลือกเฉพาะ scope ที่ระบบของคุณต้องใช้จริง (หน้าสร้างคีย์มี scope ให้เลือกมากกว่าชุดที่ v1 ใช้)
คีย์ขึ้นต้นด้วย kdoc_ และแสดงครั้งเดียวตอนสร้าง เก็บลง secret store ทันที
ยิงคำขอครั้งแรกที่ /me
endpoint นี้ไม่ต้องมี scope ใช้ยืนยันว่าคีย์ใช้ได้และผูกกับบริษัทที่ถูกต้องก่อนเขียนโค้ดจริง
ได้ 401 INVALID_API_KEY = คีย์ผิดหรือถูกเพิกถอน · ได้ 403 OPEN_API_NOT_ENABLED = แพ็กเกจยังไม่เปิด
สร้างเอกสารฉบับร่างจากระบบของคุณ
POST /documents สร้างฉบับร่างพร้อมเลขที่เอกสารทันที ทีมบัญชีตรวจและแก้ต่อในแอปได้ก่อนกดยืนยัน
ส่ง externalId ของลูกค้าและสินค้ามาด้วย ยิงซ้ำจะอัปเดตรายเดิมแทนการสร้างซ้ำ
คำขอครั้งแรก
GET /v1/me · ไม่ต้องมี scope
ทุกภาษาเรียกผ่าน HTTP lib มาตรฐาน ไม่ต้องลง SDK
GET /v1/me
curl -s "https://api.ketbill.com/v1/me" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"200 OK
{
"company": {
"id": 12,
"slug": "siam-timber",
"name": "สยามค้าไม้",
"website": "https://siamtimber.co.th"
}
}การยืนยันตัวตน
Both token types resolve through the same path and bind to exactly one company.
ทุก endpoint ใช้ header Authorization: Bearer <token> เหมือนกัน ต่างกันแค่ที่มาของ token
API key
เหมาะกับ server ของคุณเองหรือ n8n ที่ทำงานแทนบริษัทเดียว สร้างที่ https://ketbill.com → ตั้งค่า → การเชื่อมต่อ → API Keys เลือก scope ตอนสร้างแล้วแก้ทีหลังได้ คีย์ขึ้นต้นด้วย kdoc_ และแสดงเต็มครั้งเดียวตอนสร้าง
header
Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…- รูปแบบ
- JWT ลงลายเซ็น EdDSA (ไม่ใช่ opaque token)
- อายุ
- ไม่มีวันหมดอายุ — จึงไม่มี refresh token
- การเพิกถอน
- เพิกถอน salt (sid) แล้ว token ตายทันทีทุกใบของผู้ใช้นั้น
- การตรวจลายเซ็น
- verify offline ด้วย public key จาก /.well-known/jwks.json
OAuth 2.1 + PKCE (S256)
เหมาะกับแอปของบุคคลที่สามที่ผู้ใช้ KetBill ต้องอนุญาตเอง ขั้นตอนขออนุญาตเกิดบนหน้า login ของแอปตามปกติ ไม่มีหน้า consent แยกให้ผู้ใช้สับสน
- 1
สร้าง OAuth app ในแอป
ตั้งค่า → การเชื่อมต่อ → OAuth แล้วกดสร้าง ได้ client_id (และ client_secret ถ้าเลือกแบบ confidential) พร้อมตั้ง redirect URI ที่อนุญาต
- 2
ส่งผู้ใช้ไป /oauth/authorize
สร้าง code_verifier แบบสุ่ม ส่ง code_challenge = BASE64URL(SHA256(verifier)) ระบบพาผู้ใช้เข้าหน้า login ของ KetBill ตามปกติ ไม่มีหน้า consent แยก
- 3
รับ code แล้วแลกเป็น token
ผู้ใช้ล็อกอินเสร็จ ระบบ redirect กลับ redirect_uri พร้อม code แล้วคุณ POST /oauth/token ด้วย code + code_verifier
- 4
เพิกถอนเมื่อเลิกใช้
POST /oauth/revoke ลบ salt ของผู้ใช้ ทำให้ token ทุกใบของคนนั้นตายทันที
เปิดใน browser ของผู้ใช้
https://api.ketbill.com/oauth/authorize
?response_type=code
&client_id=cl_7Kd2
&redirect_uri=https://partner.example.com/callback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&scope=documents:read%20documents:create
&state=8f41c2POST /oauth/token
curl -s -X POST "https://api.ketbill.com/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "ac_3nQ",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"client_id": "cl_7Kd2",
"redirect_uri": "https://partner.example.com/callback"
}'สิทธิ์ (scopes)
A missing scope fails the whole request, never half of it.
แต่ละ endpoint ระบุ scope ที่ต้องมีไว้ตรงตัว ถ้า token ไม่มี scope นั้นจะได้ 403 SCOPE_FORBIDDEN ทั้งคำขอ ไม่มีการทำงานสำเร็จบางส่วน · ตารางนี้คือ scope ทั้งหมดที่ให้สิทธิ์ได้ ตรงกับที่หน้าสร้างคีย์และ discovery document แสดง — บางตัวเป็นของเดิม ที่ยังไม่มีเส้นทางใน v1 ที่ต้องใช้ และเขียนกำกับไว้ในแถวของมันเอง · ชุดเครื่องมือ MCP ไม่ได้ใช้ scope แต่ให้สิทธิ์เป็นราย function ตามแพ็กเกจ
| scope ที่ให้สิทธิ์ได้ | ใช้ทำอะไร |
|---|---|
| documents:read | อ่านเอกสารและ snapshot — และสร้าง ดู เพิกถอนลิงก์แชร์สาธารณะของเอกสาร (ไม่มี scope แยกสำหรับการแชร์)read documents and snapshots — and create, list and revoke their public share links; there is no separate share scope |
| documents:create | สร้างเอกสารฉบับร่างcreate draft documents |
| documents:update | แก้เอกสารที่ยังแก้ได้update editable documents |
| documents:convert | แปลงเอกสารเป็นเอกสารลูกชนิดใหม่ (ฉบับเดิมไม่ถูกแก้)convert a document into a new child document |
| documents:issue | ออกเอกสารจากฉบับร่าง เปลี่ยนสถานะเป็นออกแล้ว/รอรับชำระissue a draft — status becomes issued/awaiting payment |
| documents:mark_paid | บันทึกการชำระของเอกสารที่ออกแล้วrecord a payment on an issued document |
| documents:void | ยกเลิกเอกสารและเอกสารที่ผูกกันตามนโยบายvoid a document and its policy cascade |
| documents:delete | ลบเอกสารฉบับร่างdelete a draft document |
| documents:* | ทุกสิทธิ์ในตระกูล documents รวมของเดิมข้างบนด้วยall document scopes, legacy ones included |
| clients:read | อ่านลูกค้าและผู้ขายread clients |
| clients:create | สร้างลูกค้า (ส่ง externalId ซ้ำ = อัปเดตตัวเดิม)create clients (externalId upsert) |
| clients:update | แก้ลูกค้าที่มีอยู่ด้วย idupdate an existing client by id |
| products:read | อ่านสินค้าและบริการread products |
| products:create | สร้างหรืออัปเดตสินค้าcreate products |
| events:read | อ่านเหตุการณ์และผลการส่ง — คีย์เดิมที่มี documents:read ยังอ่านได้read events and deliveries, documents:read also accepted |
| webhooks:manage | จัดการ endpoint ของ webhook และสั่งส่งซ้ำ — คีย์เดิมที่มี documents:* ยังทำได้manage webhook endpoints and redeliver, documents:* also accepted |
| payment_accounts:read | อ่านรายชื่อบัญชีรับเงินของบริษัท เพื่อให้เลือกจาก dropdown แทนการพิมพ์ id เองread the company payment accounts, for an account picker instead of a typed id |
| reports:read | สงวนไว้สำหรับการเข้าถึงรายงาน — ให้สิทธิ์ได้ แต่ยังไม่มีเส้นทางใน v1 ที่ต้องใช้reserved for report access — grantable, no v1 route requires it |
documents:*
wildcard นี้ครอบทุก scope ในตระกูล documents: เท่านั้น ไม่ครอบลูกค้าหรือสินค้า ต้องการสองตระกูลให้เลือกทั้งสองฝั่ง
Endpoint reference
Paths, methods, params and scopes come from the live OpenAPI document; examples and prose are verified by hand.
ทั้งหมด 35 endpoint ใน 9 กลุ่ม ทุกเส้นทางต่อจาก https://api.ketbill.com กดหัวข้อเพื่อเปิดรายละเอียด หรือส่งเข้า Playground ทางขวาเพื่อยิงจริง · เส้นทาง วิธี พารามิเตอร์ และ scope มาจากสเปก ส่วนตัวอย่าง response ตรวจกับ API จริงด้วยมือ เพราะสเปกยังไม่ประกาศ response schema
Documents
10 endpointsอ่าน สร้าง และแก้เอกสารขาย ตั้งแต่ใบเสนอราคาถึงใบเสร็จ
GET/v1/documentsรายการเอกสารของบริษัทที่ token ผูกอยู่documents:read
เรียงล่าสุดก่อน · ไม่ส่ง includeTotal ค่า total เป็นตัวประมาณเพื่อความเร็ว (นับจากหน้าที่ดึงมา) ส่ง includeTotal=true เมื่อต้องการยอดรวมจริง · ยอดเงินทุกตัวเป็น string เพราะเป็น decimal ฝั่งฐานข้อมูล
| param | in | รายละเอียด |
|---|---|---|
| page | query | หน้าที่ต้องการ เริ่มที่ 1 |
| limit | query | จำนวนต่อหน้า ค่าปกติ 20 สูงสุด 100 |
| q | query | ค้นแบบมีคำนี้อยู่ข้างใน (contains) ใน 5 ช่อง: เลขที่เอกสาร, referenceCode, sourceOrderCode, ชื่อนิติบุคคลของลูกค้า และของคู่ค้า |
| type | query | ประเภทเอกสาร ค่าทั้ง 10 ตัวอยู่ในตารางค่าอ้างอิง |
| status | query | สถานะเอกสาร เทียบแบบตรงตัว ค่าทั้ง 13 ตัวอยู่ในตารางค่าอ้างอิง |
| sourceOrderCode | query | เลขออเดอร์ต้นทางจากระบบขาย เทียบแบบตรงตัวทั้งค่า ไม่ใช่บางส่วน — ใช้ถามว่า "ออเดอร์นี้ออกเอกสารไปแล้วหรือยัง" · ถ้าต้องการค้นบางส่วนของเลขออเดอร์ให้ใช้ q ซึ่งค้นคอลัมน์เดียวกันแบบ contains สองตัวนี้แทนกันไม่ได้ |
| includeTotal | query | true = นับยอดรวมจริง (ค่าอื่นถือว่าไม่ส่ง) |
GET /v1/documents
curl -s "https://api.ketbill.com/v1/documents?type=INVOICE&limit=2&includeTotal=true" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"items": [
{
"id": 8814,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0088",
"status": "WAIT_PAID",
"creationSource": "API",
"sourceOrderCode": "SO-2026-0001",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-07-26T00:00:00.000Z",
"expiryDate": null,
"customerId": 421,
"vendorId": null,
"subtotal": "150000",
"vatAmount": "10500",
"grandTotal": "160500",
"paidAmount": "0",
"remainingAmount": "160500",
"paymentMethod": null,
"parentRefs": [
{ "parent": { "type": "QUOTATION", "number": "QT-2026-07-0142" } }
],
"customer": { "id": 421, "legalName": "บริษัท สยามค้าไม้ จำกัด", "taxId": "0105548000123" },
"vendor": null,
"_count": { "lines": 4 }
}
],
"total": 128,
"page": 1,
"limit": 2
}POST/v1/documentsสร้างเอกสารฉบับร่าง (เข้าคิว — ตอบ 202)documents:create
202 ไม่ใช่ใบยืนยันว่าเอกสารจะเกิดจริง
- ตรวจเชิงธุรกิจ (VAT policy, เอกสารแม่, โควตา) รันหลังคิว ไม่ใช่ตอนตอบ 202 — poll job จนได้ DONE ก่อนเชื่อว่ามีเอกสารแล้ว
- FAILED = ผิดกฎธุรกิจ ไม่มี retry อัตโนมัติให้ — ต้องแก้ body แล้วส่งคำขอใหม่ job เดิมไม่กลับมาเป็น QUEUED เอง
- DEAD = ระบบ retry ครบรอบแล้วยังไม่ผ่าน ไม่ใช่ความผิดของผู้เรียก — แจ้งทีมงานตรวจคิวจดหมายตาย
เป็น asynchronous: ตอบ 202 พร้อม job_id และ header Location: /v1/documents/jobs/{jobId} ทันที ไม่ได้คืนตัวเอกสาร · ส่ง header Idempotency-Key กันสร้างซ้ำเวลา retry — ยิงซ้ำด้วย key เดิมได้ job_id เดิมเสมอ ไม่สร้างเอกสารซ้ำ (ไม่ส่ง = derive จาก company + body ให้ หน้าต่างกันซ้ำ 10 นาที) · ฟิลด์ที่ไม่มีใน schema ถูกตัดทิ้งเงียบ ๆ ไม่มี error สะกดชื่อฟิลด์ผิดจึงไม่มีสัญญาณเตือน · คิวไม่พร้อมใช้งาน → 503 QUEUE_UNAVAILABLE retry ด้วย key เดิมได้ทันที ไม่มีเอกสารค้าง
| param | in | รายละเอียด |
|---|---|---|
| companyId | body | ต้องมีschema บังคับให้ส่ง แต่ค่าไม่มีผล — ระบบใช้บริษัทของ token เสมอ ไม่ส่งจะได้ 400 |
| type | body | ต้องมีประเภทเอกสารที่จะสร้าง |
| customerId | body | ลูกค้าปลายทาง (ใช้ vendorId สำหรับเอกสารฝั่งซื้อ) |
| issueDate | body | ISO 8601 เต็มรูปเท่านั้น (2026-07-30T00:00:00.000Z) ส่งแบบวันที่ล้วนจะได้ 400 · ไม่ส่ง = วันนี้ |
| lines | body | ต้องมีอาร์เรย์รายการ ชื่อฟิลด์คือ lines · แต่ละบรรทัดต้องมี lineNo, description, quantity, unit, unitPrice, vatType ครบ |
| vatRate | body | อัตรา VAT ค่าปกติ 7 |
| whtMode | body | โหมดหัก ณ ที่จ่ายของเอกสาร: NONE, DOCUMENT_LEVEL, LINE_LEVEL |
POST /v1/documents
curl -s -X POST "https://api.ketbill.com/v1/documents" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"companyId": 12,
"type": "INVOICE",
"customerId": 421,
"issueDate": "2026-07-30T00:00:00.000Z",
"vatRate": 7,
"lines": [
{
"lineNo": 1,
"description": "ตู้ไม้สั่งทำพิเศษ",
"quantity": 1,
"unit": "ชุด",
"unitPrice": 120000,
"vatType": "VAT_7"
},
{
"lineNo": 2,
"description": "ค่าติดตั้งหน้างาน",
"quantity": 1,
"unit": "งาน",
"unitPrice": 35000,
"vatType": "VAT_7"
}
]
}'response
HTTP/1.1 202 Accepted
Location: /v1/documents/jobs/job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W
{
"job_id": "job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"status": "QUEUED"
}GET/v1/documents/{id}เอกสารรายฉบับ พร้อมรายการและ snapshotdocuments:read
คืนค่า snapshot ที่ล็อกไว้ตอนออกเอกสาร ไม่ใช่ข้อมูล master ปัจจุบัน · ตัวอย่างด้านล่างตัดมาบางฟิลด์ ของจริงคืน lines, payments, parentRefs/childRefs, issuerSnapshot/partySnapshot ครบทั้งใบ
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
GET /v1/documents/{id}
curl -s "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"id": 8814,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0088",
"status": "WAIT_PAID",
"creationSource": "API",
"sourceOrderCode": "SO-2026-0001",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-07-26T00:00:00.000Z",
"expiryDate": null,
"referenceCode": null,
"customerId": 421,
"vendorId": null,
"issuerSnapshot": {},
"partySnapshot": {},
"issuerSetting": null,
"currency": "THB",
"exchangeRate": "1",
"vatInclusive": false,
"vatRate": "7",
"whtRate": null,
"whtRateId": null,
"whtMode": "NONE",
"taxInvoiceCombined": false,
"whtCertBookNo": null,
"whtCertPndForm": null,
"whtCertFormSeqNo": null,
"whtCertRemitMethod": null,
"whtCertRemitOther": null,
"whtCertFundGpf": null,
"whtCertFundSso": null,
"whtCertFundPvd": null,
"whtCertDividendRate": null,
"whtCertDividendOther": null,
"subtotal": "150000",
"discountAmount": "0",
"discountPercent": null,
"taxableAmount": "150000",
"vatAmount": "10500",
"whtAmount": "0",
"grandTotal": "160500",
"grandTotalText": "หนึ่งแสนหกหมื่นห้าร้อยบาทถ้วน",
"paidAmount": "0",
"remainingAmount": "160500",
"paymentDate": null,
"paymentBankRef": null,
"paymentNote": null,
"remark": null,
"internalNote": null,
"paymentTerms": "14 วันหลังตอบรับ",
"paymentMethod": null,
"receivedDate": null,
"chequeNo": null,
"chequeDate": null,
"chequeBank": null,
"voidedAt": null,
"voidedReason": null,
"createdById": null,
"updatedById": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-12T03:00:00.000Z",
"deletedAt": null,
"editingByUserId": null,
"editingByName": null,
"editingExpiresAt": null,
"customer": { "id": 421, "legalName": "บริษัท สยามค้าไม้ จำกัด" },
"vendor": null,
"lines": [
{
"id": 55201,
"documentId": 8814,
"lineNo": 1,
"productId": null,
"productSnapshot": null,
"description": "ตู้ไม้สั่งทำพิเศษ",
"note": null,
"quantity": "1",
"unit": "ชุด",
"productDetail": null,
"whtIncomeType": null,
"whtDividendCase": null,
"whtPaidOn": null,
"unitPrice": "150000",
"discountAmount": "0",
"discountPercent": null,
"vatType": "VAT_7",
"vatAmount": "10500",
"whtRate": null,
"whtRateId": null,
"whtCategory": null,
"whtAmount": "0",
"lineTotal": "160500",
"sourceLineId": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-12T03:00:00.000Z"
}
],
"payments": [],
"receiptPayments": [],
"parentRefs": [
{
"id": 901,
"parentId": 8790,
"childId": 8814,
"relationType": "CONVERSION",
"note": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"parent": { "id": 8790, "type": "QUOTATION", "number": "QT-2026-07-0142", "status": "CONVERTED" }
}
],
"childRefs": [],
"attachments": [],
"actionPolicy": {
"canEdit": false,
"editReason": "ใบแจ้งหนี้แก้ไขได้เฉพาะแบบร่าง",
"editCode": "INVOICE_DRAFT_ONLY",
"canVoid": true,
"voidReason": null,
"voidCode": "VOID_ALLOWED",
"canIssue": false,
"issueReason": "ออกเอกสารได้เฉพาะเอกสารแบบร่าง",
"canMarkPaid": true,
"markPaidReason": null,
"canConvertTo": ["BILLING_NOTE", "RECEIPT", "TAX_INVOICE"]
},
"lineage": [
{ "id": 8790, "type": "QUOTATION", "number": "QT-2026-07-0142", "status": "CONVERTED" },
{ "id": 8814, "type": "INVOICE", "number": "INV-2026-07-0088", "status": "WAIT_PAID" }
],
"relatedDocuments": {
"nodes": [
{ "id": 8790, "type": "QUOTATION", "number": "QT-2026-07-0142", "status": "CONVERTED", "issueDate": "2026-07-05T00:00:00.000Z", "grandTotal": "160500", "distance": 1 }
],
"edges": [
{ "parentId": 8790, "childId": 8814, "relationType": "CONVERSION" }
],
"total": 1
},
"issuableRemaining": null
}PUT/v1/documents/{id}แก้ไขเอกสารที่ยังแก้ได้documents:update
สามกรณีที่ยังติด 409 แม้เอกสารเป็นฉบับร่าง
- ใบกำกับภาษีที่ออกจากใบแจ้งหนี้หรือใบเสร็จ — TAX_INVOICE_SOURCE_LOCKED ต้องแก้ที่เอกสารต้นทาง
- ใบลดหนี้ที่อ้างใบกำกับภาษี — ส่งได้แค่ remark กับ internalNote ฟิลด์อื่นตอบ CREDIT_NOTE_SOURCE_LOCKED
- มีคนเปิดแก้ใบนั้นอยู่ในแอป — LOCK_HELD เส้นนี้ไม่ต้องขอ edit lock เอง แต่ทับคนที่ถืออยู่ไม่ได้
ส่งมาเฉพาะฟิลด์ที่ต้องการแก้ ฟิลด์ที่ไม่ส่งคงค่าเดิม · แก้ได้เฉพาะฉบับร่าง สถานะอื่นตอบ 409 พร้อม code บอกเหตุผล · ส่ง lines มาคือแทนที่รายการทั้งชุด ไม่ใช่การเพิ่มบรรทัด · ยอดเงินคำนวณใหม่ให้จาก lines ที่ส่งมา
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
| remark, internalNote | body | หมายเหตุบนเอกสาร และโน้ตภายในที่ไม่พิมพ์ออกเอกสาร |
| issueDate, dueDate | body | ISO 8601 เต็มรูป · dueDate ส่ง null เพื่อล้างกำหนดชำระ ส่วน issueDate ล้างไม่ได้ |
| customerId, vendorId | body | เปลี่ยนคู่ค้าของเอกสาร ส่ง null เพื่อถอดออก |
| lines | body | แทนที่รายการทั้งชุด แต่ละบรรทัดต้องมี lineNo, description, quantity, unit, unitPrice, vatType ครบเหมือนตอนสร้าง |
| whtMode, whtRate, whtRateId | body | โหมดและอัตราหักภาษี ณ ที่จ่าย ดูค่าที่รับได้ในตารางค่าอ้างอิง |
| vatInclusive | body | true = ราคาที่ส่งมารวม VAT แล้ว |
| paymentTerms, paymentMethod, receiptPayment | body | เงื่อนไขและวิธีชำระ กับข้อมูลการรับชำระบนใบเสร็จ |
| referenceCode | body | รหัสอ้างอิงของคุณเอง ใช้ค้นด้วย q ได้ |
PUT /v1/documents/{id}
curl -s -X PUT "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"remark": "แก้ตามที่ลูกค้าขอเพิ่มค่าติดตั้ง",
"dueDate": "2026-08-15T00:00:00.000Z",
"lines": [
{
"lineNo": 1,
"description": "ตู้ไม้สั่งทำพิเศษ",
"quantity": 1,
"unit": "ชุด",
"unitPrice": 120000,
"vatType": "VAT_7"
}
]
}'response
{
"id": 8814,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0088",
"status": "DRAFT",
"creationSource": "API",
"sourceOrderCode": "SO-2026-0001",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-08-15T00:00:00.000Z",
"expiryDate": null,
"referenceCode": null,
"customerId": 421,
"vendorId": null,
"issuerSnapshot": {},
"partySnapshot": {},
"issuerSetting": null,
"currency": "THB",
"exchangeRate": "1",
"vatInclusive": false,
"vatRate": "7",
"whtRate": null,
"whtRateId": null,
"whtMode": "NONE",
"taxInvoiceCombined": false,
"whtCertBookNo": null,
"whtCertPndForm": null,
"whtCertFormSeqNo": null,
"whtCertRemitMethod": null,
"whtCertRemitOther": null,
"whtCertFundGpf": null,
"whtCertFundSso": null,
"whtCertFundPvd": null,
"whtCertDividendRate": null,
"whtCertDividendOther": null,
"subtotal": "120000",
"discountAmount": "0",
"discountPercent": null,
"taxableAmount": "120000",
"vatAmount": "8400",
"whtAmount": "0",
"grandTotal": "128400",
"grandTotalText": "หนึ่งแสนสองหมื่นแปดพันสี่ร้อยบาทถ้วน",
"paidAmount": "0",
"remainingAmount": "128400",
"paymentDate": null,
"paymentBankRef": null,
"paymentNote": null,
"remark": "แก้ตามที่ลูกค้าขอเพิ่มค่าติดตั้ง",
"internalNote": null,
"paymentTerms": null,
"paymentMethod": null,
"receivedDate": null,
"chequeNo": null,
"chequeDate": null,
"chequeBank": null,
"voidedAt": null,
"voidedReason": null,
"createdById": null,
"updatedById": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T10:20:00.000Z",
"deletedAt": null,
"editingByUserId": null,
"editingByName": null,
"editingExpiresAt": null,
"customer": { "id": 421, "legalName": "บริษัท สยามค้าไม้ จำกัด" },
"vendor": null,
"lines": [
{
"id": 55301,
"documentId": 8814,
"lineNo": 1,
"productId": null,
"productSnapshot": null,
"description": "ตู้ไม้สั่งทำพิเศษ",
"note": null,
"quantity": "1",
"unit": "ชุด",
"productDetail": null,
"whtIncomeType": null,
"whtDividendCase": null,
"whtPaidOn": null,
"unitPrice": "120000",
"discountAmount": "0",
"discountPercent": null,
"vatType": "VAT_7",
"vatAmount": "8400",
"whtRate": null,
"whtRateId": null,
"whtCategory": null,
"whtAmount": "0",
"lineTotal": "128400",
"sourceLineId": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T10:20:00.000Z"
}
],
"payments": [],
"receiptPayments": [],
"parentRefs": [],
"childRefs": []
}DELETE/v1/documents/{id}ลบเอกสารฉบับร่างdocuments:delete
ลบแบบ soft-delete และทำได้เฉพาะฉบับร่างตามนโยบายของ service — ออกเอกสารไปแล้วให้ใช้ยกเลิกแทน · สำเร็จตอบ 204 ไม่มีเนื้อหา · ลบไปแล้ว 409 ALREADY_DELETED · ฉบับร่างที่มีคนกำลังแก้ในแอป 409 LOCK_HELD
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
DELETE /v1/documents/{id}
curl -s -X DELETE "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
ไม่มีเนื้อหาตอบกลับ (204)
GET/v1/documents/jobs/{jobId}สถานะงานสร้างเอกสารที่เข้าคิวไว้documents:read
สถานะไต่ QUEUED → PROCESSING → DONE | FAILED | DEAD · DONE แล้วใช้ document_id ไปเรียก GET /v1/documents/{id} ต่อได้ · FAILED = ผิดกฎธุรกิจ ไม่มี retry ให้ · DEAD = ระบบ retry ครบรอบแล้วยังไม่ผ่าน ทีมงานเห็นในคิวจดหมายตาย · อ่านจาก NATS KV เป็นหลัก แล้วตกไปอ่าน MySQL เมื่อ KV หมดอายุ 24 ชั่วโมง · MySQL ไม่รับโหลดนี้เลย แต่ API มี rate limit 30 req/s ต่อ key (เกินแล้ว 429 พร้อม Retry-After) · poll ทุก 1 วินาทีเกินพอ — วัดจริง (2026-08-03) client-visible p50=73ms / p95=87ms ไม่ใช่ SLO
| param | in | รายละเอียด |
|---|---|---|
| jobId | path | ต้องมีjob id ที่ได้จาก job_id หรือ header Location ตอน POST /documents ตอบ 202 |
GET /v1/documents/jobs/{jobId}
curl -s "https://api.ketbill.com/v1/documents/jobs/job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"job_id": "job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"status": "DONE",
"document_id": 8815,
"document_no": "INV-2026-07-0089",
"error": null
}POST/v1/documents/{id}/convertสร้างเอกสารลูกจากเอกสารนี้documents:convert
สร้างเอกสารใหม่ตาม childType โดยยกคู่ค้าและบรรทัดมาจากต้นทาง — เอกสารต้นทางไม่ถูกแก้เนื้อหา · ตอบ 201 พร้อมเอกสารลูกฉบับร่าง และ parentRefs ชี้กลับไปที่ต้นทาง · สถานะต้นทางยังแปลงไม่ได้ 409 DOCUMENT_CONVERT_NOT_ALLOWED · ใบแจ้งหนี้/ใบวางบิลที่ยังไม่รับชำระแปลงเป็นใบเสร็จไม่ได้ 409 DOCUMENT_CONVERT_PAYMENT_REQUIRED · บริษัทที่ยังไม่จดทะเบียน VAT ออกใบกำกับภาษีไม่ได้ 403
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสารต้นทาง ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
| childType | body | ต้องมีชนิดเอกสารลูกที่จะสร้าง ค่าทั้ง 10 ตัวอยู่ในตารางค่าอ้างอิง |
| lineSelections | body | เลือกบรรทัดและจำนวนที่ยกไปเอกสารลูก ไม่ส่ง = ยกทั้งฉบับ |
| issueDate | body | วันที่ของเอกสารลูก ISO 8601 เต็มรูป ไม่ส่ง = วันนี้ |
POST /v1/documents/{id}/convert
curl -s -X POST "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"childType": "INVOICE"
}'response
{
"id": 8815,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0091",
"status": "DRAFT",
"creationSource": "API",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-08-15T00:00:00.000Z",
"expiryDate": null,
"referenceCode": null,
"customerId": 421,
"vendorId": null,
"issuerSnapshot": {},
"partySnapshot": {},
"issuerSetting": null,
"currency": "THB",
"exchangeRate": "1",
"vatInclusive": false,
"vatRate": "7",
"whtRate": null,
"whtRateId": null,
"whtMode": "NONE",
"subtotal": "120000",
"discountAmount": "0",
"discountPercent": null,
"taxableAmount": "120000",
"vatAmount": "8400",
"whtAmount": "0",
"grandTotal": "128400",
"grandTotalText": "หนึ่งแสนสองหมื่นแปดพันสี่ร้อยบาทถ้วน",
"paidAmount": "0",
"remainingAmount": "128400",
"paymentDate": null,
"paymentBankRef": null,
"paymentNote": null,
"remark": null,
"internalNote": null,
"paymentTerms": null,
"paymentMethod": null,
"receivedDate": null,
"chequeNo": null,
"chequeDate": null,
"chequeBank": null,
"voidedAt": null,
"voidedReason": null,
"createdById": null,
"updatedById": null,
"createdAt": "2026-07-30T11:20:00.000Z",
"updatedAt": "2026-07-30T11:20:00.000Z",
"deletedAt": null,
"editingByUserId": null,
"editingByName": null,
"editingExpiresAt": null,
"customer": {
"id": 421,
"legalName": "บริษัท สยามค้าไม้ จำกัด"
},
"vendor": null,
"lines": [
{
"id": 55340,
"documentId": 8815,
"lineNo": 1,
"productId": null,
"productSnapshot": null,
"description": "ตู้ไม้สั่งทำพิเศษ",
"note": null,
"quantity": "1",
"unit": "ชุด",
"unitPrice": "120000",
"discountAmount": "0",
"discountPercent": null,
"vatType": "VAT_7",
"vatAmount": "8400",
"whtRate": null,
"whtRateId": null,
"whtCategory": null,
"whtAmount": "0",
"lineTotal": "128400",
"sourceLineId": 55301,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T10:20:00.000Z"
}
],
"payments": [],
"receiptPayments": [],
"parentRefs": [
{
"id": 8814,
"type": "QUOTATION",
"number": "QT-2026-07-0042",
"status": "ISSUED"
}
],
"childRefs": []
}POST/v1/documents/{id}/issueออกเอกสารจากฉบับร่างdocuments:issue
เปลี่ยนสถานะจากร่างเป็นออกแล้ว/รอรับชำระตามชนิดเอกสาร · เลขที่เอกสารมีตั้งแต่ตอนสร้างและไม่เปลี่ยนตอนออก · ออกได้เฉพาะฉบับร่าง — ออกไปแล้ว 409 ALREADY_ISSUED สถานะอื่น 409 ISSUE_NOT_ALLOWED · ใบกำกับภาษีแบบตรงต้องแนบ receiptPayment ให้ครบ ไม่ครบ 409 TAX_INVOICE_PAYMENT_* · มีคนเปิดแก้ใบนั้นในแอป 409 LOCK_HELD · ส่ง header Idempotency-Key ได้เพื่อกันออกซ้ำ
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
| receiptPayment | body | ข้อมูลการรับชำระที่ต้องแนบเมื่อออกใบกำกับภาษีแบบตรง — เอกสารชนิดอื่นไม่ต้องส่ง |
POST /v1/documents/{id}/issue
curl -s -X POST "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{}'response
{
"id": 8814,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0088",
"status": "ISSUED",
"creationSource": "API",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-08-15T00:00:00.000Z",
"expiryDate": null,
"referenceCode": null,
"customerId": 421,
"vendorId": null,
"issuerSnapshot": {},
"partySnapshot": {},
"issuerSetting": null,
"currency": "THB",
"exchangeRate": "1",
"vatInclusive": false,
"vatRate": "7",
"whtRate": null,
"whtRateId": null,
"whtMode": "NONE",
"subtotal": "120000",
"discountAmount": "0",
"discountPercent": null,
"taxableAmount": "120000",
"vatAmount": "8400",
"whtAmount": "0",
"grandTotal": "128400",
"grandTotalText": "หนึ่งแสนสองหมื่นแปดพันสี่ร้อยบาทถ้วน",
"paidAmount": "0",
"remainingAmount": "128400",
"paymentDate": null,
"paymentBankRef": null,
"paymentNote": null,
"remark": "แก้ตามที่ลูกค้าขอเพิ่มค่าติดตั้ง",
"internalNote": null,
"paymentTerms": null,
"paymentMethod": null,
"receivedDate": null,
"chequeNo": null,
"chequeDate": null,
"chequeBank": null,
"voidedAt": null,
"voidedReason": null,
"createdById": null,
"updatedById": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T11:05:00.000Z",
"deletedAt": null,
"editingByUserId": null,
"editingByName": null,
"editingExpiresAt": null,
"customer": {
"id": 421,
"legalName": "บริษัท สยามค้าไม้ จำกัด"
},
"vendor": null,
"lines": [
{
"id": 55301,
"documentId": 8814,
"lineNo": 1,
"productId": null,
"productSnapshot": null,
"description": "ตู้ไม้สั่งทำพิเศษ",
"note": null,
"quantity": "1",
"unit": "ชุด",
"unitPrice": "120000",
"discountAmount": "0",
"discountPercent": null,
"vatType": "VAT_7",
"vatAmount": "8400",
"whtRate": null,
"whtRateId": null,
"whtCategory": null,
"whtAmount": "0",
"lineTotal": "128400",
"sourceLineId": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T10:20:00.000Z"
}
],
"payments": [],
"receiptPayments": [],
"parentRefs": [],
"childRefs": []
}POST/v1/documents/{id}/mark-paidบันทึกรับชำระของเอกสารที่ออกแล้วdocuments:mark_paid
บันทึกยอดที่รับจริง — ยอดเต็มปิดเป็นรับชำระแล้ว ยอดบางส่วนคงสถานะรับชำระบางส่วน · รายการที่บันทึกโผล่ใน payments ของเอกสาร · ทำได้เฉพาะเอกสารที่รอรับชำระ สถานะอื่น 409 MARK_PAID_NOT_ALLOWED · ยอดเกินคงเหลือ 409 · ฉบับร่างยังบันทึกไม่ได้ ต้องออกเอกสารก่อน
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
| amount | body | ต้องมียอดที่รับจริง ห้ามเกินยอดคงเหลือของเอกสาร |
| paidDate | body | วันที่รับชำระ ISO 8601 เต็มรูป ไม่ส่ง = วันนี้ |
| method | body | วิธีชำระ เป็นข้อความอิสระ ไม่ใช่ enum |
| bankRef | body | เลขอ้างอิงจากธนาคาร |
| note | body | หมายเหตุของรายการชำระ |
POST /v1/documents/{id}/mark-paid
curl -s -X POST "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"amount": 128400,
"paidDate": "2026-08-05T00:00:00.000Z",
"method": "transfer",
"bankRef": "TRF-2026-08-05-0007",
"note": "ชำระเต็มจำนวน"
}'response
{
"id": 8814,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0088",
"status": "PAID",
"creationSource": "API",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-08-15T00:00:00.000Z",
"expiryDate": null,
"referenceCode": null,
"customerId": 421,
"vendorId": null,
"issuerSnapshot": {},
"partySnapshot": {},
"issuerSetting": null,
"currency": "THB",
"exchangeRate": "1",
"vatInclusive": false,
"vatRate": "7",
"whtRate": null,
"whtRateId": null,
"whtMode": "NONE",
"subtotal": "120000",
"discountAmount": "0",
"discountPercent": null,
"taxableAmount": "120000",
"vatAmount": "8400",
"whtAmount": "0",
"grandTotal": "128400",
"grandTotalText": "หนึ่งแสนสองหมื่นแปดพันสี่ร้อยบาทถ้วน",
"paidAmount": "128400",
"remainingAmount": "0",
"paymentDate": "2026-08-05T00:00:00.000Z",
"paymentBankRef": "TRF-2026-08-05-0007",
"paymentNote": "ชำระเต็มจำนวน",
"remark": "แก้ตามที่ลูกค้าขอเพิ่มค่าติดตั้ง",
"internalNote": null,
"paymentTerms": null,
"paymentMethod": "transfer",
"receivedDate": null,
"chequeNo": null,
"chequeDate": null,
"chequeBank": null,
"voidedAt": null,
"voidedReason": null,
"createdById": null,
"updatedById": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-08-05T09:12:00.000Z",
"deletedAt": null,
"editingByUserId": null,
"editingByName": null,
"editingExpiresAt": null,
"customer": {
"id": 421,
"legalName": "บริษัท สยามค้าไม้ จำกัด"
},
"vendor": null,
"lines": [
{
"id": 55301,
"documentId": 8814,
"lineNo": 1,
"productId": null,
"productSnapshot": null,
"description": "ตู้ไม้สั่งทำพิเศษ",
"note": null,
"quantity": "1",
"unit": "ชุด",
"unitPrice": "120000",
"discountAmount": "0",
"discountPercent": null,
"vatType": "VAT_7",
"vatAmount": "8400",
"whtRate": null,
"whtRateId": null,
"whtCategory": null,
"whtAmount": "0",
"lineTotal": "128400",
"sourceLineId": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T10:20:00.000Z"
}
],
"payments": [
{
"id": 4471,
"documentId": 8814,
"receiptId": null,
"paidDate": "2026-08-05T00:00:00.000Z",
"amount": "128400",
"method": "transfer",
"bankRef": "TRF-2026-08-05-0007",
"note": "ชำระเต็มจำนวน",
"createdAt": "2026-08-05T09:12:00.000Z"
}
],
"receiptPayments": [],
"parentRefs": [],
"childRefs": []
}POST/v1/documents/{id}/voidยกเลิกเอกสารพร้อมเอกสารที่สืบต่อdocuments:void
ยกเลิกแล้วลามถึงเอกสารลูกด้วย ไม่ใช่แค่ฉบับที่ระบุ
- voidSummary.cancelledDocumentIds บอกครบว่าฉบับไหนถูกยกเลิกไปบ้าง — อ่านก่อนแจ้งผู้ใช้
- เอกสารลูกที่รับชำระแล้วก็ถูกยกเลิกไปด้วยเมื่อ policy สั่งให้ลาม
- ยกเลิกแล้วย้อนกลับไม่ได้ ต้องออกเอกสารใหม่แทน
ยกเลิกเอกสารและเอกสารที่สืบต่อจากมันตามนโยบายของระบบ · voidSummary บอกจำนวนและ id ที่ถูกยกเลิกจริง · ยกเลิกไม่ได้ตามนโยบาย 409 พร้อม code จาก void-policy · ฉบับร่างที่มีคนกำลังแก้ในแอป 409 LOCK_HELD
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
| reason | body | เหตุผลที่ยกเลิก เก็บลง voidedReason ของเอกสาร |
POST /v1/documents/{id}/void
curl -s -X POST "https://api.ketbill.com/v1/documents/8814" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"reason": "ยกเลิกตามคำขอของลูกค้า"
}'response
{
"id": 8814,
"companyId": 12,
"type": "INVOICE",
"number": "INV-2026-07-0088",
"status": "CANCELLED",
"creationSource": "API",
"issueDate": "2026-07-12T00:00:00.000Z",
"dueDate": "2026-08-15T00:00:00.000Z",
"expiryDate": null,
"referenceCode": null,
"customerId": 421,
"vendorId": null,
"issuerSnapshot": {},
"partySnapshot": {},
"issuerSetting": null,
"currency": "THB",
"exchangeRate": "1",
"vatInclusive": false,
"vatRate": "7",
"whtRate": null,
"whtRateId": null,
"whtMode": "NONE",
"subtotal": "120000",
"discountAmount": "0",
"discountPercent": null,
"taxableAmount": "120000",
"vatAmount": "8400",
"whtAmount": "0",
"grandTotal": "128400",
"grandTotalText": "หนึ่งแสนสองหมื่นแปดพันสี่ร้อยบาทถ้วน",
"paidAmount": "0",
"remainingAmount": "128400",
"paymentDate": null,
"paymentBankRef": null,
"paymentNote": null,
"remark": "แก้ตามที่ลูกค้าขอเพิ่มค่าติดตั้ง",
"internalNote": null,
"paymentTerms": null,
"paymentMethod": null,
"receivedDate": null,
"chequeNo": null,
"chequeDate": null,
"chequeBank": null,
"voidedAt": "2026-08-05T09:40:12.480Z",
"voidedReason": "ยกเลิกตามคำขอของลูกค้า",
"createdById": null,
"updatedById": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-08-05T09:40:12.480Z",
"deletedAt": null,
"editingByUserId": null,
"editingByName": null,
"editingExpiresAt": null,
"customer": {
"id": 421,
"legalName": "บริษัท สยามค้าไม้ จำกัด"
},
"vendor": null,
"lines": [
{
"id": 55301,
"documentId": 8814,
"lineNo": 1,
"productId": null,
"productSnapshot": null,
"description": "ตู้ไม้สั่งทำพิเศษ",
"note": null,
"quantity": "1",
"unit": "ชุด",
"unitPrice": "120000",
"discountAmount": "0",
"discountPercent": null,
"vatType": "VAT_7",
"vatAmount": "8400",
"whtRate": null,
"whtRateId": null,
"whtCategory": null,
"whtAmount": "0",
"lineTotal": "128400",
"sourceLineId": null,
"createdAt": "2026-07-12T03:00:00.000Z",
"updatedAt": "2026-07-30T10:20:00.000Z"
}
],
"payments": [],
"receiptPayments": [],
"parentRefs": [],
"childRefs": [],
"voidSummary": {
"cancelledDocumentIds": [
8814,
8815
],
"cancelledCount": 2,
"cascaded": true
}
}Clients
3 endpointsลูกค้าและผู้ขาย พร้อม upsert ด้วย externalId ของระบบคุณ
GET/v1/clientsรายการลูกค้าและผู้ขายclients:read
endpoint นี้คืนอาร์เรย์ตรง ๆ ไม่มี total/page/limit ห่อไว้ — อ่านผลจาก response ทั้งก้อน ไม่ใช่ res.items · แต่ละแถวพ่วง addresses, clientPersons และ accountManager มาด้วย (ตัวอย่างตัดมาบางฟิลด์)
| param | in | รายละเอียด |
|---|---|---|
| page | query | หน้าที่ต้องการ เริ่มที่ 1 |
| limit | query | จำนวนต่อหน้า ค่าปกติ 100 สูงสุด 200 |
| q | query | ค้นชื่อจดทะเบียน ชื่อเรียก เลขผู้เสียภาษี รหัสภายใน อีเมล เบอร์โทร รวมถึงชื่อผู้ติดต่อ |
| kind | query | CUSTOMER, VENDOR หรือ BOTH · กรอง CUSTOMER แล้วแถว BOTH ติดมาด้วยเสมอ |
GET /v1/clients
curl -s "https://api.ketbill.com/v1/clients?q=siam&limit=2" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
[
{
"id": 421,
"companyId": 12,
"accountManagerId": null,
"kind": "CUSTOMER",
"code": "CUS-0042",
"customCode": null,
"externalId": "shop-cust-4821",
"legalName": "บริษัท สยามค้าไม้ จำกัด",
"displayName": "สยามค้าไม้",
"ownerName": null,
"clientType": "COMPANY",
"taxId": "0105548000123",
"branchCode": "00000",
"branchName": "สำนักงานใหญ่",
"email": "[email protected]",
"phone": "02-301-5555",
"fax": null,
"website": null,
"lineId": null,
"creditLimit": null,
"creditMonths": null,
"creditDays": 30,
"defaultWhtRate": null,
"notes": null,
"logoUrl": null,
"signatureUrl": null,
"stampUrl": null,
"isActive": true,
"createdAt": "2026-01-15T00:00:00.000Z",
"updatedAt": "2026-01-15T00:00:00.000Z",
"deletedAt": null,
"addresses": [
{
"id": 88,
"clientId": 421,
"label": "สำนักงานใหญ่",
"isPrimary": true,
"isBilling": true,
"isShipping": true,
"branchCode": null,
"branchName": null,
"addressLine1": "313 ถนนสีลม",
"addressLine2": null,
"subDistrict": "แขวงสีลม",
"district": "เขตบางรัก",
"province": "กรุงเทพมหานคร",
"postalCode": "10400",
"country": "TH",
"createdAt": "2026-01-15T00:00:00.000Z"
}
],
"clientPersons": [
{
"id": 61,
"clientId": 421,
"name": "คุณ อรพรรณ พรหมมา",
"position": null,
"email": "[email protected]",
"phone": "02-301-5556",
"isPrimary": true,
"createdAt": "2026-01-15T00:00:00.000Z"
}
],
"accountManager": null
}
]POST/v1/clientsสร้างลูกค้า หรืออัปเดตด้วย externalId เดิมclients:create
ส่ง externalId ของระบบต้นทางมาด้วย — ถ้าเคยส่งแล้วระบบอัปเดตรายเดิมและตอบ 200 ถ้าเป็นรายใหม่ตอบ 201 (ยิงซ้ำได้ไม่เกิดข้อมูลซ้ำ) · ฟิลด์ที่ไม่มีใน schema ถูกตัดทิ้งเงียบ ๆ ไม่มี error เหมือนเส้นสร้างเอกสาร
| param | in | รายละเอียด |
|---|---|---|
| kind | body | ต้องมีCUSTOMER, VENDOR หรือ BOTH — ไม่มีค่าปกติ ไม่ส่งจะได้ 400 |
| legalName | body | ต้องมีชื่อตามหนังสือรับรอง |
| taxId | body | เลขผู้เสียภาษี 13 หลัก |
| externalId | body | id ของลูกค้ารายนี้ในระบบต้นทาง |
| addresses | body | ที่อยู่จดทะเบียนและที่อยู่จัดส่ง |
POST /v1/clients
curl -s -X POST "https://api.ketbill.com/v1/clients" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"kind": "CUSTOMER",
"legalName": "บริษัท สยามค้าไม้ จำกัด",
"taxId": "0105548000123",
"branchCode": "00000",
"externalId": "shop-cust-4821",
"phone": "021234567"
}'response
{
"id": 421,
"companyId": 12,
"accountManagerId": null,
"kind": "CUSTOMER",
"code": "CUS-0042",
"customCode": null,
"displayName": null,
"ownerName": null,
"clientType": "COMPANY",
"legalName": "บริษัท สยามค้าไม้ จำกัด",
"taxId": "0105548000123",
"branchCode": "00000",
"branchName": "สำนักงานใหญ่",
"email": null,
"phone": "021234567",
"fax": null,
"website": null,
"lineId": null,
"creditLimit": null,
"creditMonths": null,
"creditDays": null,
"defaultWhtRate": null,
"notes": null,
"logoUrl": null,
"signatureUrl": null,
"stampUrl": null,
"externalId": "shop-cust-4821",
"isActive": true,
"createdAt": "2026-07-30T10:00:00.000Z",
"updatedAt": "2026-07-30T10:00:00.000Z",
"deletedAt": null,
"addresses": [],
"clientPersons": [],
"accountManager": null
}PUT/v1/clients/{id}แก้ไขลูกค้าด้วย idclients:update
addresses กับ clientPersons เป็นการแทนที่ทั้งชุด ไม่ใช่การเพิ่มรายการ
- ส่งมาเมื่อไหร่ ของเดิมถูกลบทิ้งทั้งหมดก่อนเขียนชุดใหม่ — แถวใหม่ได้ id ใหม่ อ้าง id เดิมต่อไม่ได้
- ไม่ส่งฟิลด์นั้นมาเลย = คงของเดิมไว้ครบ · ส่งอาเรย์ว่างคือสั่งลบให้เหลือศูนย์รายการ
- ที่อยู่มีที่อยู่หลักได้ชุดละหนึ่งรายการ — ไม่ระบุ isPrimary ระบบตั้งรายการแรกให้ ระบุมาหลายรายการเหลือรายการแรกที่ระบุ
เส้นนี้อ้าง id เท่านั้น ไม่ใช่ upsert ตาม externalId เหมือน POST · ส่งมาเฉพาะฟิลด์ที่ต้องการแก้ ฟิลด์ที่ไม่ส่งคงค่าเดิม · ฟิลด์ที่ไม่มีใน schema ถูกตัดทิ้งเงียบ ๆ ไม่มี error เหมือนเส้นสร้างลูกค้า · ไม่พบ คนละบริษัท หรือถูกลบไปแล้ว ตอบ 404 NOT_FOUND
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของลูกค้า ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
| kind | body | CUSTOMER, VENDOR หรือ BOTH |
| legalName, displayName | body | ชื่อตามหนังสือรับรอง และชื่อที่ใช้เรียกสั้น ๆ |
| taxId, branchCode | body | เลขผู้เสียภาษี 13 หลัก และรหัสสาขา 5 หลัก |
| phone, email | body | ส่ง null เพื่อล้างค่าเดิม |
| externalId | body | id ของลูกค้ารายนี้ในระบบต้นทาง แก้ได้ แต่เส้นนี้ไม่ได้ใช้มันค้นหา |
| isActive | body | false = ปิดใช้งาน ไม่ใช่การลบ ลูกค้ายังอยู่ในรายการ |
| addresses | body | แทนที่ที่อยู่ทั้งชุด ดูคำเตือนด้านบน |
| clientPersons | body | แทนที่ผู้ติดต่อทั้งชุด ดูคำเตือนด้านบน |
PUT /v1/clients/{id}
curl -s -X PUT "https://api.ketbill.com/v1/clients/421" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"phone": "021234567",
"addresses": [
{
"label": "สำนักงานใหญ่",
"addressLine1": "313 ถนนสีลม",
"subDistrict": "แขวงสีลม",
"district": "เขตบางรัก",
"province": "กรุงเทพมหานคร",
"postalCode": "10500",
"country": "TH"
}
]
}'response
{
"id": 421,
"companyId": 12,
"accountManagerId": null,
"kind": "CUSTOMER",
"code": "CUS-0042",
"customCode": null,
"externalId": "shop-cust-4821",
"legalName": "บริษัท สยามค้าไม้ จำกัด",
"displayName": "สยามค้าไม้",
"ownerName": null,
"clientType": "COMPANY",
"taxId": "0105548000123",
"branchCode": "00000",
"branchName": "สำนักงานใหญ่",
"email": "[email protected]",
"phone": "021234567",
"fax": null,
"website": null,
"lineId": null,
"creditLimit": null,
"creditMonths": null,
"creditDays": 30,
"defaultWhtRate": null,
"notes": null,
"logoUrl": null,
"signatureUrl": null,
"stampUrl": null,
"isActive": true,
"createdAt": "2026-01-15T00:00:00.000Z",
"updatedAt": "2026-07-30T11:05:00.000Z",
"deletedAt": null,
"addresses": [
{
"id": 92,
"clientId": 421,
"label": "สำนักงานใหญ่",
"isPrimary": true,
"isBilling": true,
"isShipping": false,
"branchCode": null,
"branchName": null,
"addressLine1": "313 ถนนสีลม",
"addressLine2": null,
"subDistrict": "แขวงสีลม",
"district": "เขตบางรัก",
"province": "กรุงเทพมหานคร",
"postalCode": "10500",
"country": "TH",
"createdAt": "2026-07-30T11:05:00.000Z"
}
],
"clientPersons": [
{
"id": 61,
"clientId": 421,
"name": "คุณ อรพรรณ พรหมมา",
"position": null,
"email": "[email protected]",
"phone": "02-301-5556",
"isPrimary": true,
"createdAt": "2026-01-15T00:00:00.000Z"
}
],
"accountManager": null
}Products
3 endpointsสินค้าและบริการที่ใช้เป็นรายการในเอกสาร
GET/v1/productsรายการสินค้าและบริการproducts:read
ใช้ดึงราคาและหน่วยล่าสุดก่อนสร้างเอกสาร · คืนอาร์เรย์ตรง ๆ ไม่มี total/page/limit ห่อไว้ เหมือน /clients · ราคาเป็น string เพราะเป็น decimal ฝั่งฐานข้อมูล ส่วน stock เป็นตัวเลข
| param | in | รายละเอียด |
|---|---|---|
| page | query | หน้าที่ต้องการ เริ่มที่ 1 |
| limit | query | จำนวนต่อหน้า ค่าปกติ 100 สูงสุด 100 |
| q | query | ค้นชื่อสินค้า SKU บาร์โค้ด หรือคำอธิบาย |
| type | query | GOODS หรือ SERVICE |
GET /v1/products
curl -s "https://api.ketbill.com/v1/products?q=cabinet&limit=2" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
[
{
"id": 1180,
"companyId": 12,
"sku": "WD-CAB-01",
"barcode": null,
"externalId": null,
"name": "ตู้ไม้สั่งทำพิเศษ",
"description": null,
"unit": "ชุด",
"defaultPrice": "120000",
"costPrice": "86000",
"stock": 4,
"vatType": "VAT_7",
"productType": "GOODS",
"isActive": true,
"createdAt": "2026-01-10T00:00:00.000Z",
"updatedAt": "2026-01-10T00:00:00.000Z",
"deletedAt": null
}
]POST/v1/productsสร้างสินค้า หรืออัปเดตด้วย externalId เดิมproducts:create
รูปแบบ upsert เดียวกับลูกค้า — externalId ซ้ำคืออัปเดต ไม่ใช่สร้างใหม่ · เส้นนี้เข้มกว่า: body รับเฉพาะฟิลด์ที่ประกาศไว้ ส่งฟิลด์แปลกปลอมมาจะได้ 400 (ต่างจากเส้นสร้างเอกสารและสร้างลูกค้าที่ตัดทิ้งเงียบ ๆ)
| param | in | รายละเอียด |
|---|---|---|
| name | body | ต้องมีชื่อสินค้าหรือบริการ — ฟิลด์เดียวที่บังคับ |
| unit | body | หน่วยนับที่จะขึ้นบนเอกสาร · ไม่ส่ง = ชิ้น |
| defaultPrice | body | ราคาตั้งต้นต่อหน่วย · ไม่ส่ง = 0 |
| sku | body | รหัสสินค้าของคุณ |
| vatType | body | VAT_7, VAT_0 หรือ NON_VAT · ไม่ส่ง = ตามสถานะจดทะเบียน VAT ของบริษัท |
POST /v1/products
curl -s -X POST "https://api.ketbill.com/v1/products" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"name": "ค่าติดตั้งหน้างาน",
"unit": "งาน",
"defaultPrice": 35000,
"vatType": "VAT_7",
"productType": "SERVICE",
"externalId": "shop-sku-install"
}'response
{
"id": 1181,
"companyId": 12,
"sku": null,
"barcode": null,
"externalId": "shop-sku-install",
"name": "ค่าติดตั้งหน้างาน",
"description": null,
"unit": "งาน",
"defaultPrice": "35000",
"costPrice": null,
"stock": null,
"vatType": "VAT_7",
"productType": "SERVICE",
"isActive": true,
"createdAt": "2026-07-30T10:00:00.000Z",
"updatedAt": "2026-07-30T10:00:00.000Z",
"deletedAt": null
}GET/v1/products/{id}สินค้ารายตัวproducts:read
ไม่พบสินค้าในบริษัทของ token จะได้ 404 NOT_FOUND
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีid ของสินค้า ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422 |
GET /v1/products/{id}
curl -s "https://api.ketbill.com/v1/products/1180" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"id": 1180,
"companyId": 12,
"sku": "WD-CAB-01",
"barcode": null,
"externalId": null,
"name": "ตู้ไม้สั่งทำพิเศษ",
"description": null,
"unit": "ชุด",
"defaultPrice": "120000",
"costPrice": "86000",
"stock": 4,
"vatType": "VAT_7",
"productType": "GOODS",
"isActive": true,
"createdAt": "2026-01-10T00:00:00.000Z",
"updatedAt": "2026-01-10T00:00:00.000Z",
"deletedAt": null
}Me
1 endpointsตรวจว่า token ผูกกับบริษัทไหน
GET/v1/meบริษัทที่ token ใบนี้ผูกอยู่auth เท่านั้น
ไม่ต้องมี scope — เรียกครั้งแรกด้วย endpoint นี้เพื่อยืนยันว่าคีย์ใช้ได้และผูกกับบริษัทที่ถูกต้อง
ส่งเข้า PlaygroundGET /v1/me
curl -s "https://api.ketbill.com/v1/me" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"company": {
"id": 12,
"slug": "siam-timber",
"name": "สยามค้าไม้",
"website": "https://siamtimber.co.th"
}
}OAuth 2.1
6 endpointsauthorization-code + PKCE สำหรับแอปที่ออก token แทนผู้ใช้
GET/oauth/authorizeจุดเริ่ม flow — พาผู้ใช้เข้าหน้า login ของ KetBillauth เท่านั้น
เปิด URL นี้ใน browser ของผู้ใช้ ไม่ใช่เรียกจาก server · ระบบพาผู้ใช้เข้าหน้า login ปกติของแอป (flow เดียวกับการเชื่อมต่อจาก Ketshopweb) ผู้ใช้ล็อกอินและเลือกบริษัทตามปกติ แล้ว redirect กลับ redirect_uri พร้อม ?code=…&state=…
| param | in | รายละเอียด |
|---|---|---|
| response_type | query | ต้องมีcode |
| client_id | query | ต้องมีclient_id ของ OAuth app ที่สร้างในแอป |
| redirect_uri | query | ต้องมีต้องตรงกับที่ตั้งไว้ใน OAuth app |
| code_challenge | query | ต้องมีBASE64URL(SHA256(code_verifier)) |
| code_challenge_method | query | ต้องมีS256 |
| scope | query | scope ที่ขอ คั่นด้วยช่องว่าง |
| state | query | ค่าสุ่มที่คุณตรวจตอน redirect กลับ |
เปิดใน browser ของผู้ใช้
https://api.ketbill.com/oauth/authorize
?response_type=code
&client_id=cl_7Kd2
&redirect_uri=https://partner.example.com/callback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&scope=documents:read%20documents:create
&state=8f41c2response
HTTP/1.1 302 Found
Location: https://partner.example.com/callback?code=ac_3nQ…&state=8f41c2POST/oauth/tokenแลก code เป็น access tokenauth เท่านั้น
confidential client ส่ง client_secret ใน body (client_secret_post) ไม่ใช่ header Basic · access token ไม่มีวันหมดอายุ จึงไม่มี refresh token
| param | in | รายละเอียด |
|---|---|---|
| grant_type | body | ต้องมีauthorization_code |
| code | body | ต้องมีcode ที่ได้จากขั้น authorize |
| code_verifier | body | ต้องมีค่าจริงของ code_challenge |
| client_id | body | ต้องมีclient_id ของ OAuth app |
| client_secret | body | เฉพาะ confidential client |
POST /oauth/token
curl -s -X POST "https://api.ketbill.com/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "ac_3nQ",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"client_id": "cl_7Kd2",
"redirect_uri": "https://partner.example.com/callback"
}'response
{
"access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6ImtiLTIwMjYtMDcifQ…",
"token_type": "Bearer",
"scope": "documents:read documents:create"
}POST/oauth/revokeเพิกถอน token (RFC 7009)auth เท่านั้น
ลบ per-user salt ของ token นั้น ทำให้ access token ทุกใบของผู้ใช้รายนั้นใช้ไม่ได้ทันที · ตอบ 200 เสมอ แม้ token ไม่มีอยู่
| param | in | รายละเอียด |
|---|---|---|
| token | body | ต้องมีaccess token ที่ต้องการเพิกถอน |
POST /oauth/revoke
curl -s -X POST "https://api.ketbill.com/oauth/revoke" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJFZERTQSJ9"
}'response
HTTP/1.1 200 OKGET/.well-known/jwks.jsonpublic key สำหรับ verify token แบบ offlineauth เท่านั้น
จับคู่ key ด้วย kid ใน JWT header · cache ได้ (Cache-Control: max-age=3600) จึงไม่ต้องเรียก introspect ทุกคำขอ
GET /.well-known/jwks.json
curl -s "https://api.ketbill.com/.well-known/jwks.json"response
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"kid": "kb-2026-07",
"use": "sig",
"alg": "EdDSA"
}
]
}GET/.well-known/oauth-authorization-servermetadata ของ authorization server (RFC 8414)auth เท่านั้น
ให้ OAuth client ตั้งค่าเองอัตโนมัติ — endpoints, PKCE method, auth methods และ scope ที่รองรับ
GET /.well-known/oauth-authorization-server
curl -s "https://api.ketbill.com/.well-known/oauth-authorization-server"response
{
"issuer": "https://api.ketbill.com",
"authorization_endpoint": "https://api.ketbill.com/oauth/authorize",
"token_endpoint": "https://api.ketbill.com/oauth/token",
"revocation_endpoint": "https://api.ketbill.com/oauth/revoke",
"jwks_uri": "https://api.ketbill.com/.well-known/jwks.json",
"code_challenge_methods_supported": ["S256"]
}GET/.well-known/oauth-protected-resourcemetadata ของ protected resource (RFC 9728)auth เท่านั้น
บอกว่า /v1 เชื่อถือ authorization server ใด และรับ bearer token ทาง header
GET /.well-known/oauth-protected-resource
curl -s "https://api.ketbill.com/.well-known/oauth-protected-resource"response
{
"resource": "https://api.ketbill.com/v1",
"authorization_servers": ["https://api.ketbill.com"],
"bearer_methods_supported": ["header"]
}Payment accounts
1 endpointsGET/v1/payment-accountsบัญชีรับเงินของบริษัทที่ token ผูกอยู่payment_accounts:read
มีไว้ให้ทำ dropdown เลือกบัญชี แทนการให้ผู้ใช้พิมพ์ id เอง · คืนทั้งหมดครั้งเดียว ไม่แบ่งหน้า (บัญชีรับเงินต่อบริษัทมีหลักหน่วย) เรียงตามลำดับที่สร้างไว้ในหน้าตั้งค่า · บริษัทที่ยังไม่ได้ตั้งบัญชีได้ items เป็น array ว่าง ไม่ใช่ 404 · บัญชีที่ปิดใช้งานแล้วยังคืนมาด้วย isActive: false เพื่อให้ค่าที่ผูกไว้เดิมยังแสดงชื่อได้ — กรองเองถ้าไม่อยากให้เลือกใหม่ · สองบัญชีในชื่อนิติบุคคลเดียวกันจะเห็นเป็นสองแถวชื่อซ้ำ ให้แยกด้วย bankName กับ accountNumber
ส่งเข้า PlaygroundGET /v1/payment-accounts
curl -s "https://api.ketbill.com/v1/payment-accounts" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"items": [
{
"id": 12,
"name": "บริษัท เดโม จำกัด",
"type": "bank",
"bankName": "กสิกรไทย",
"accountNumber": "123-4-56789-0",
"isPrimary": true,
"isActive": true,
"systemKey": null
},
{
"id": 13,
"name": "บริษัท เดโม จำกัด",
"type": "promptpay",
"bankName": null,
"accountNumber": null,
"isPrimary": false,
"isActive": true,
"systemKey": null
},
{
"id": 41,
"name": "เงินรอรับ Marketplace",
"type": "bank",
"bankName": null,
"accountNumber": null,
"isPrimary": false,
"isActive": true,
"systemKey": "mp_holding"
}
]
}Events
3 endpointsGET/v1/eventsประวัติ event ของบริษัท เรียงใหม่ไปเก่าevents:read
ไล่หน้าด้วย cursor ไม่ใช่ page — ส่ง next_cursor ที่ได้กลับไปที่ query cursor เพื่อขอหน้าถัดไป · แต่ละรายการแนบผลการส่งเข้า webhook ทุก endpoint ไว้ใน deliveries · คีย์เก่าที่มี scope documents:read หรือ documents:* ก็ยังเรียกได้ (เผื่อความเข้ากันได้กับคีย์ที่ออกก่อนมี events:read)
| param | in | รายละเอียด |
|---|---|---|
| type | query | document.created หรือ document.failed — ไม่ส่ง = ทุกชนิด |
| cursor | query | id ของ event ตัวสุดท้ายที่ได้จากหน้าก่อน ไม่ส่ง = เริ่มจากล่าสุด |
| limit | query | จำนวนต่อหน้า ค่าปกติ 20 สูงสุด 100 |
GET /v1/events
curl -s "https://api.ketbill.com/v1/events?type=document.created&limit=2" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"items": [
{
"id": "evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"type": "document.created",
"created_at": "2026-07-30T10:15:00.000Z",
"job_id": "job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"job_status": "DONE",
"document_id": 8815,
"document_no": "INV-2026-07-0089",
"client_name": "บริษัท สยามค้าไม้ จำกัด",
"amount": 165850,
"error_code": null,
"error_message": null,
"source": "ERP บัญชี (API key)",
"summary": "ออกใบแจ้งหนี้ INV-2026-07-0089",
"payload": {
"v": 1,
"eventId": "evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"type": "document.created",
"companyId": 12,
"jobId": "job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"documentId": 8815,
"documentNo": "INV-2026-07-0089",
"createdAt": "2026-07-30T10:15:00.000Z"
},
"deliveries": [
{
"delivery_id": "whd_5f3a1b",
"endpoint_id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"state": "delivered",
"attempts": [
{ "no": 1, "at": "2026-07-30T10:15:01.000Z", "httpStatus": 200, "durationMs": 436, "outcome": "delivered" }
],
"attempt_count": 1,
"last_http_status": 200,
"last_error": null,
"delivered_at": "2026-07-30T10:15:01.000Z",
"next_retry_at": null,
"updated_at": "2026-07-30T10:15:01.000Z"
}
]
}
],
"next_cursor": null
}GET/v1/events/{id}event รายตัว พร้อมประวัติการส่งรายครั้งevents:read
deliveries[].attempts เก็บทุกครั้งที่ยิง (เวลา, HTTP, เวลาที่ใช้, ผล) · state retrying หมายถึงยังมีนัดยิงใหม่ที่ next_retry_at · payload คือก้อน JSON เดียวกับที่ยิงเข้า webhook จริง (byte เท่ากันทุกตัวอักษร) — แต่เป็น camelCase (eventId, jobId, documentId, documentNo, companyId, createdAt) คนละชุดกับฟิลด์ snake_case รอบนอกก้อนนี้ ไม่มีคีย์ document_id อยู่ข้างในเลย · คีย์เก่าที่มี scope documents:read หรือ documents:* ก็ยังเรียกได้ (เผื่อความเข้ากันได้กับคีย์ที่ออกก่อนมี events:read)
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีevent id (evt_...) จาก GET /v1/events หรือจาก payload ที่ยิงเข้า webhook |
GET /v1/events/{id}
curl -s "https://api.ketbill.com/v1/events/evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"id": "evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"type": "document.created",
"created_at": "2026-07-30T10:15:00.000Z",
"job_id": "job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"job_status": "DONE",
"document_id": 8815,
"document_no": "INV-2026-07-0089",
"client_name": "บริษัท สยามค้าไม้ จำกัด",
"amount": 165850,
"error_code": null,
"error_message": null,
"source": "ERP บัญชี (API key)",
"summary": "ออกใบแจ้งหนี้ INV-2026-07-0089",
"payload": {
"v": 1,
"eventId": "evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"type": "document.created",
"companyId": 12,
"jobId": "job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"documentId": 8815,
"documentNo": "INV-2026-07-0089",
"createdAt": "2026-07-30T10:15:00.000Z"
},
"deliveries": [
{
"delivery_id": "whd_5f3a1b",
"endpoint_id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"state": "delivered",
"attempts": [
{ "no": 1, "at": "2026-07-30T10:15:01.000Z", "httpStatus": 200, "durationMs": 436, "outcome": "delivered" }
],
"attempt_count": 1,
"last_http_status": 200,
"last_error": null,
"delivered_at": "2026-07-30T10:15:01.000Z",
"next_retry_at": null,
"updated_at": "2026-07-30T10:15:01.000Z"
}
]
}POST/v1/events/{id}/redeliverสั่งส่ง event นี้เข้า webhook อีกครั้งwebhooks:manage
ตอบ 202 แล้วส่งเบื้องหลัง — ผลจริงดูที่ GET /v1/events/{id} อีกที · บันทึกเป็นความพยายามครั้งใหม่ ไม่ทับผลของครั้งก่อน · ไม่ส่ง endpoint_id = ส่งซ้ำให้ทุก endpoint ที่ยังเปิดใช้งานและสมัคร event ชนิดนี้ · endpoint ที่ถูกปิดอัตโนมัติต้องเปิดใช้งาน (PUT status=ACTIVE) ก่อน ไม่งั้น 409 ENDPOINT_DISABLED · ใช้ scope webhooks:manage ไม่ใช่ events:read เพราะเป็นการยิง HTTP ออกนอกจริง · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีevent id (evt_...) ที่จะส่งซ้ำ |
| endpoint_id | body | ส่งเฉพาะ endpoint นี้ ไม่ส่ง = ส่งซ้ำทุก endpoint ที่สมัคร event ชนิดนี้ |
POST /v1/events/{id}/redeliver
curl -s -X POST "https://api.ketbill.com/v1/events/evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W/redeliver" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"endpoint_id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W"
}'response
{
"event_id": "evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"queued": [
{ "delivery_id": "whd_7a2c9f", "endpoint_id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W" }
]
}Webhooks
5 endpointsGET/v1/webhooksรายการ webhook endpoint ของบริษัทwebhooks:manage
secret ไม่ถูกคืนอีกหลังตอนสร้าง เหลือแค่ secret_preview · จำกัดสูงสุด 5 endpoint ต่อบริษัท · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)
ส่งเข้า PlaygroundGET /v1/webhooks
curl -s "https://api.ketbill.com/v1/webhooks" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"items": [
{
"id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"url": "https://erp.example.com/hooks/ketbill",
"description": "ERP บัญชี",
"events": ["document.created", "document.failed"],
"status": "active",
"secret_preview": "whsec_9c2f1a••••7b41",
"created_at": "2026-07-20T09:00:00.000Z",
"disabled_at": null,
"delivered": 1284,
"failed": 3,
"pending": 0,
"last_attempt_at": "2026-07-30T10:15:01.000Z",
"last_http_status": 200,
"type": "user"
}
],
"max": 5,
"used": 1
}POST/v1/webhooksลงทะเบียน webhook endpoint ใหม่webhooks:manage
จำกัด 5 endpoint ต่อบริษัท เกินแล้ว 409 ENDPOINT_LIMIT · secret เต็มออกครั้งนี้ครั้งเดียว เก็บทันที ระบบไม่แสดงซ้ำอีก · ทุกครั้งที่ยิงจะมี header X-KetBill-Signature: t=<unixts>,v1=<hex> โดย v1 = HMAC-SHA256 ของสตริง <t>.<rawBody> ด้วย secret เต็ม (รวม prefix whsec_) ฝั่งรับควรตรวจ t ไม่ให้เก่าเกิน 5 นาที แล้วเทียบ v1 แบบ constant-time · ระบบยิงแบบ at-least-once ฝั่งรับต้อง dedupe เองด้วย event id · body ที่ปลายทางได้รับคือก้อน JSON เดียวกับ payload ของ GET /v1/events/{id} เป๊ะ (camelCase — ไม่มีคีย์ document_id) เช่น {"v":1,"eventId":"evt_01JZ8Q2K3M4N5P6Q7R8S9T0V1W","type":"document.created","companyId":12,"jobId":"job_01JZ8Q2K3M4N5P6Q7R8S9T0V1W","documentId":8815,"documentNo":"INV-2026-07-0089","createdAt":"2026-07-30T10:15:00.000Z"} — type=document.failed ไม่มี documentId/documentNo แต่มี error:{code,message} แทน dedupe ด้วย eventId ตามนี้ · ไม่ตาม redirect — ตอบ 3xx ถือว่าส่งล้มเหลว ตั้ง URL ปลายทางสุดท้ายมาตรง ๆ · ส่ง URL ที่ไม่ใช่ https (เช่น http ธรรมดา) → 400 VALIDATION ทันที รับเฉพาะ https เท่านั้น · เป็น https แล้วแต่ปลายทางเป็นที่อยู่ไม่สาธารณะ — loopback/เครือข่ายภายใน/link-local (รวม metadata ของ cloud) หรือชื่อ localhost/.local/.internal — → 400 URL_NOT_ALLOWED (คนละโค้ดกับข้อบน) และระบบตรวจซ้ำทุกครั้งก่อนส่งจริง ปลายทางที่เปลี่ยน DNS มาชี้ภายในทีหลังก็ถูกปฏิเสธเช่นกัน · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)
| param | in | รายละเอียด |
|---|---|---|
| url | body | ต้องมีปลายทาง ต้องเป็น https และเป็นที่อยู่สาธารณะเท่านั้น |
| events | body | ต้องมีarray อย่างน้อย 1 ตัว ค่าที่รับได้: document.created, document.failed |
| description | body | ป้ายกำกับช่วยจำ ไม่กระทบการทำงาน |
POST /v1/webhooks
curl -s -X POST "https://api.ketbill.com/v1/webhooks" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.com/hooks/ketbill",
"events": ["document.created", "document.failed"],
"description": "ERP บัญชี"
}'response
{
"endpoint": {
"id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"url": "https://erp.example.com/hooks/ketbill",
"description": "ERP บัญชี",
"events": ["document.created", "document.failed"],
"status": "active",
"secret_preview": "whsec_9c2f1a••••7b41",
"created_at": "2026-07-30T10:00:00.000Z",
"disabled_at": null,
"delivered": 0,
"failed": 0,
"pending": 0,
"last_attempt_at": null,
"last_http_status": null,
"type": "user"
},
"secret": "whsec_4f1Ck2Vb9QpZ7mR3sT6yXw8NuE0aD5gH"
}PUT/v1/webhooks/{id}แก้ไข webhook endpoint หรือเปิดใช้งานอีกครั้งwebhooks:manage
ส่งมาเฉพาะฟิลด์ที่ต้องการแก้ ฟิลด์ที่ไม่ส่งคงค่าเดิม · ส่ง status=ACTIVE เพื่อเปิดใช้งาน endpoint ที่ถูกปิดอัตโนมัติ (ล้างนาฬิกา 24 ชั่วโมงให้ด้วย) · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีwebhook endpoint id (whe_...) ต้องเป็นของบริษัทเดียวกับ token |
| url | body | เปลี่ยนปลายทาง ต้องเป็น https และเป็นที่อยู่สาธารณะเหมือนตอนสร้าง |
| events | body | แทนที่ชุด event ทั้งหมด ไม่ใช่การเพิ่ม |
| description | body | ส่ง null เพื่อลบป้ายกำกับ |
| status | body | ส่งได้ค่าเดียวคือ ACTIVE ใช้เปิดใช้งาน endpoint ที่ถูกปิดอัตโนมัติ |
PUT /v1/webhooks/{id}
curl -s -X PUT "https://api.ketbill.com/v1/webhooks/whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"status": "ACTIVE"
}'response
{
"id": "whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W",
"url": "https://erp.example.com/hooks/ketbill",
"description": "ERP บัญชี",
"events": ["document.created", "document.failed"],
"status": "active",
"secret_preview": "whsec_9c2f1a••••7b41",
"created_at": "2026-07-20T09:00:00.000Z",
"disabled_at": null,
"delivered": 1284,
"failed": 3,
"pending": 0,
"last_attempt_at": "2026-07-30T10:15:01.000Z",
"last_http_status": 200,
"type": "user"
}DELETE/v1/webhooks/{id}ลบ webhook endpointwebhooks:manage
ลบแล้วเหตุการณ์ใหม่ไม่ถูกส่งมาที่นี่อีก และรายการที่ค้างรอส่งซ้ำถูกยกเลิก · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีwebhook endpoint id (whe_...) ที่จะลบ |
DELETE /v1/webhooks/{id}
curl -s -X DELETE "https://api.ketbill.com/v1/webhooks/whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"deleted": true
}POST/v1/webhooks/{id}/testยิงทดสอบไปที่ endpoint นี้webhooks:manage
ส่ง payload ตัวอย่าง type=ping ที่ลงลายเซ็นจริงด้วย secret ของ endpoint นั้น เพื่อตรวจว่าปลายทางรับได้ · ไม่ถูกนับเป็นประวัติการส่งจริงและไม่กระทบตัวนับที่ใช้ปิดอัตโนมัติ · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)
| param | in | รายละเอียด |
|---|---|---|
| id | path | ต้องมีwebhook endpoint id (whe_...) ที่จะยิงทดสอบ |
POST /v1/webhooks/{id}/test
curl -s -X POST "https://api.ketbill.com/v1/webhooks/whe_01JZ8Q2K3M4N5P6Q7R8S9T0V1W/test" \
-H "Authorization: Bearer kdoc_6f2c91a4e08d_9f2c…"response
{
"ok": true,
"http_status": 200,
"duration_ms": 436,
"outcome": "delivered",
"cause": null,
"error": null
}ตารางค่าอ้างอิง
Every closed value set the API accepts, with the Thai meaning each one carries in the app.
ค่าพวกนี้เป็นชุดปิด ตัวพิมพ์ใหญ่ทั้งหมด และเทียบแบบตรงตัว ส่งค่านอกชุดมาในบอดี้จะถูกปฏิเสธตอน validate ส่วนการกรองผ่าน query ด้วยค่านอกชุดไม่ error แต่จะกรองไม่เจอข้อมูล
DocumentType
10 ค่าfield type ตอนสร้างเอกสาร · query type ตอนกรองรายการ
| QUOTATION | ใบเสนอราคาquotation |
| INVOICE | ใบแจ้งหนี้invoice |
| TAX_INVOICE | ใบกำกับภาษี — มักออกรวมกับใบเสร็จเป็นใบกำกับภาษี/ใบเสร็จรับเงินtax invoice |
| RECEIPT | ใบเสร็จรับเงินreceipt |
| CREDIT_NOTE | ใบลดหนี้credit note |
| BILLING_NOTE | ใบวางบิลbilling note |
| PURCHASE_ORDER | ใบสั่งซื้อ — ออกให้ผู้ขายpurchase order |
| GOODS_RECEIPT | ใบรับของgoods receipt |
| DELIVERY_NOTE | ใบส่งของdelivery note |
| MATERIAL_REQUEST | ใบเบิกของmaterial request |
DocumentStatus
13 ค่าfield status ในทุก response ของเอกสาร · query status ตอนกรองรายการ
| DRAFT | ร่างdraft |
| ISSUED | ออกแล้ว — แก้ไม่ได้อีกissued, immutable |
| PARTIALLY_ISSUED | ออกบางส่วนpartially issued |
| SUCCESS | สำเร็จทั้ง journeyjourney complete |
| WAIT_ACCEPTED | รอคู่ค้าตอบรับใบเสนอราคาawaiting acceptance |
| ACCEPTED | ลูกค้าตอบรับใบเสนอราคาaccepted |
| REJECTED | ลูกค้าปฏิเสธใบเสนอราคาrejected |
| WAIT_PAID | รอชำระเงินawaiting payment |
| PARTIALLY_PAID | ชำระบางส่วนใบแจ้งหนี้ / ใบกำกับภาษีpartially paid |
| PAID | ชำระครบแล้วpaid in full |
| OVERDUE | เลยกำหนดชำระoverdue |
| CANCELLED | ยกเลิกก่อนออกเอกสารcancelled before issue |
| CONVERTED | แปลงเป็นเอกสารอื่นแล้ว เช่นใบเสนอราคา → ใบแจ้งหนี้converted |
WhtMode
field whtMode ตอนสร้างเอกสาร · ค่าปกติคือ DOCUMENT_LEVEL
- NONE
- ไม่หักภาษี ณ ที่จ่ายno withholding
- DOCUMENT_LEVEL
- หักระดับเอกสาร — ใช้ whtRate ของเอกสารทั้งฉบับone rate for the document
- LINE_LEVEL
- หักระดับรายการ — ใช้ whtRate ของแต่ละบรรทัดper-line rates
VatType
field vatType ของรายการในเอกสาร และของสินค้า
- VAT_7
- มี VAT 7%7% VAT
- VAT_0
- VAT 0% — งานส่งออกzero-rated
- NON_VAT
- ยกเว้น VATVAT-exempt
ClientKind
field kind ตอนสร้างลูกค้า · query kind ตอนกรองรายการ
- CUSTOMER
- ลูกค้า ฝั่งขาย — รหัสขึ้นต้น CUS-customer
- VENDOR
- ผู้ขาย ฝั่งซื้อ — รหัสขึ้นต้น VEN-vendor
- BOTH
- ทั้งสองบทบาท — รหัสขึ้นต้น BTH- และติดมาด้วยทุกครั้งที่กรอง kind อื่นboth roles
ProductType
field productType ตอนสร้างสินค้า · query type ตอนกรองรายการ
- GOODS
- สินค้า — นับสต็อกgoods, stock tracked
- SERVICE
- บริการ — ไม่นับสต็อก ค่า stock เป็น nullservice, no stock
โครงของ array ในเอกสาร
สามฟิลด์ที่เป็น array ของ object ในเอกสารรายฉบับ — โครงมาจาก schema เดียวกับที่ API ใช้ตรวจ response จริง ฟิลด์ที่เขียนว่า null ได้ คือค่าที่ระบบคืน null จริง ไม่ใช่ฟิลด์ที่หายไปจากผลลัพธ์
attachments[]
อยู่ใน GET /v1/documents/{id} เท่านั้น — PUT ไม่คืนฟิลด์นี้
- id
- id ของไฟล์แนบinteger
- documentId
- id ของเอกสารที่ไฟล์นี้แนบอยู่integer
- fileName
- ชื่อไฟล์ที่อัปโหลดstring
- fileUrl
- URL สำหรับดาวน์โหลดไฟล์string
- mimeType
- ชนิดไฟล์ · null เมื่อระบบไม่ได้บันทึกไว้string | null
- fileSize
- ขนาดไฟล์เป็น byte · null เมื่อไม่ทราบinteger | null
- uploadedAt
- เวลาที่อัปโหลด ISO 8601string
payments[]
รายการชำระที่ผูกกับเอกสารนี้ · เพิ่มขึ้นเมื่อบันทึกรับชำระ
- id
- id ของรายการชำระinteger
- documentId
- id ของเอกสารที่รับชำระinteger
- receiptId
- id ของใบเสร็จที่ลงรายการนี้ · null เมื่อยังไม่ถูกลงใบเสร็จinteger | null
- paidDate
- วันที่ชำระ ISO 8601string
- amount
- จำนวนเงินที่ชำระ · เป็น string เพราะเป็น decimal ฝั่งฐานข้อมูลstring
- method
- วิธีชำระ เป็นข้อความอิสระ ไม่ใช่ enumstring
- bankRef
- เลขอ้างอิงจากธนาคาร · null เมื่อไม่ได้ระบุstring | null
- note
- หมายเหตุของรายการชำระ · null เมื่อไม่ได้ระบุstring | null
- createdAt
- เวลาที่บันทึกรายการ ISO 8601string
- sourceDocumentNumber
- เลขที่เอกสารต้นทาง · มีเฉพาะ payments ของใบเสร็จร่างที่ระบบเติมให้จากใบแจ้งหนี้ต้นทาง ฟิลด์นี้ไม่โผล่ในกรณีอื่นstring (optional)
receiptPayments[]
รายการชำระฝั่งใบเสร็จ (receiptId = เอกสารนี้) · โครงเดียวกับ payments แต่ไม่มี sourceDocumentNumber
- id
- id ของรายการชำระinteger
- documentId
- id ของเอกสารต้นทางที่ถูกชำระinteger
- receiptId
- id ของใบเสร็จฉบับนี้integer | null
- paidDate
- วันที่ชำระ ISO 8601string
- amount
- จำนวนเงินที่ชำระ · string แบบเดียวกับยอดอื่นในเอกสารstring
- method
- วิธีชำระ เป็นข้อความอิสระstring
- bankRef
- เลขอ้างอิงจากธนาคาร · null เมื่อไม่ได้ระบุstring | null
- note
- หมายเหตุของรายการชำระ · null เมื่อไม่ได้ระบุstring | null
- createdAt
- เวลาที่บันทึกรายการ ISO 8601string
Error และ rate limit
Every failure uses one envelope; the limit is 30 requests per second per key.
ทุก error ตอบด้วย envelope เดียวกัน เขียน handler ครั้งเดียวใช้ได้ทุก endpoint ค่า code ใช้ตัดสินใจในโค้ด ส่วน message เป็นภาษาไทยพร้อมโชว์ผู้ใช้ · คำขอถูกตรวจสองชั้น ชนิดข้อมูลที่ขอบตอบ 422 ส่วนกฎของ schema ตอบ 400 — ทั้งสองใช้ code เดียวกันคือ VALIDATION ให้ดูเลขสถานะประกอบ
| http | code | ความหมาย |
|---|---|---|
| 400 | VALIDATION | ฟิลด์บังคับขาด หรือส่งฟิลด์ที่ไม่มีใน schema มาที่เส้นที่ไม่รับฟิลด์เกิน · มี issues บอกชื่อฟิลด์ที่ผิดมาด้วย |
| 401 | AUTH_REQUIRED | ไม่ได้แนบ token มาใน header |
| 401 | INVALID_API_KEY | token ผิด หมดอายุ หรือถูกเพิกถอนแล้ว |
| 403 | OPEN_API_NOT_ENABLED | แพ็กเกจของบริษัทยังไม่เปิด Open API |
| 403 | SCOPE_FORBIDDEN | token ไม่มี scope ที่ endpoint นี้ต้องใช้ |
| 404 | NOT_FOUND | ไม่พบข้อมูลในบริษัทของ token |
| 422 | VALIDATION | ชนิดข้อมูลผิดที่ขอบ เช่น id ไม่ใช่จำนวนเต็ม หรือ boolean ส่งมาเป็นข้อความ · message เป็นรายละเอียดจากตัวตรวจ schema |
| 429 | RATE_LIMITED | เรียกถี่เกิน 30 ครั้งต่อวินาที ต่อคีย์ API หรือต่อ access token ดู Retry-After |
error envelope
{ "error": { "code": "SCOPE_FORBIDDEN", "message": "ไม่มีสิทธิ์ที่ต้องใช้" } }400 VALIDATION
{
"error": {
"code": "VALIDATION",
"message": "ข้อมูลไม่ถูกต้อง",
"issues": [
{ "code": "unrecognized_keys", "keys": ["totalAmount"], "path": [] }
]
}
}429 RATE_LIMITED
HTTP/1.1 429 Too Many Requests
Retry-After: 1
{ "error": { "code": "RATE_LIMITED", "message": "เรียกถี่เกินกำหนด" } }เกินลิมิต 30 req/s จะได้ 429 พร้อม Retry-After เป็นวินาที ให้รอตามค่านั้นแล้วยิงซ้ำ อย่าวน retry ทันที · เฉพาะ 400 ที่มี issues บอกชื่อฟิลด์ที่ผิดมาให้ ส่วน 422 ใส่รายละเอียดไว้ใน message
409 — มีเฉพาะบนเส้นแก้เอกสาร PUT /documents/{id}
ทุกตัวหมายถึงคำขอถูกปฏิเสธก่อนเขียน ข้อมูลเดิมไม่ถูกแก้ · retry ไม่ช่วย ยกเว้น LOCK_HELD
| <ชนิด>_DRAFT_ONLY | เอกสารพ้นฉบับร่างแล้ว เช่น QUOTATION_DRAFT_ONLY, INVOICE_DRAFT_ONLY, TAX_INVOICE_DRAFT_ONLY และแบบย่อ PO_, GR_, DN_, MR_DRAFT_ONLY |
| STATUS_LOCKED | เอกสารถูกยกเลิกแล้ว |
| TAX_INVOICE_SOURCE_LOCKED | ใบกำกับภาษีที่ออกจากใบแจ้งหนี้หรือใบเสร็จ ต้องแก้ที่เอกสารต้นทาง — ตอบแม้ยังเป็นฉบับร่าง |
| CREDIT_NOTE_SOURCE_LOCKED | ใบลดหนี้ที่อ้างใบกำกับภาษี แก้ได้แค่ remark กับ internalNote |
| LOCK_HELD | มีคนเปิดแก้เอกสารนี้ในแอปอยู่ รอให้ปิดหรือหมดเวลาแล้วลองใหม่ |
MCP
เปิดใช้แล้วMCP server ของ KetBill ให้ผู้ช่วย AI อ่านรายงาน ค้นเอกสาร และร่างเอกสารใหม่แทนคุณได้ โดยสิทธิ์มาจากแพ็กเกจเป็นราย function ไม่ใช่ scope แบบ Open API เริ่มใช้สามขั้น — (1) สร้างการเชื่อมต่อชนิด MCP ในหน้าการเชื่อมต่อของแอป คีย์ชนิดนี้เรียก Open API ไม่ได้และไม่ถือ scope (2) ชี้ MCP client มาที่ endpoint ด้านล่าง แล้วต่อด้วย OAuth 2.1 + PKCE (3) ดูว่าใช้ฟังก์ชันไหนไปเท่าไหร่แล้วได้ที่แท็บ โควตา MCP ในหน้านักพัฒนา ส่วนผู้ดูแลระบบตั้งเพดานต่อแพ็กเกจให้ได้ · การเรียกสองฟังก์ชันรายงานต้องเปิดสิทธิ์รายงานบน credential ใบนั้นก่อน
endpoint
https://ketbill.com/mcp
คนละโฮสต์กับ Open API โดยตั้งใจ — token ของ MCP ผูก audience ไว้กับโฮสต์ที่ออก token ให้ URL นี้จึงต้องเป็นโฮสต์เดียวกับที่ทำ OAuth · discovery อยู่ที่ /.well-known/oauth-protected-resource/mcp ของโฮสต์เดียวกัน
- การยืนยันตัวตน
- OAuth 2.1 + PKCE หรือคีย์ชนิด MCP ที่สร้างในหน้าการเชื่อมต่อ · discovery อยู่ที่ /.well-known/oauth-protected-resource/mcp ของโฮสต์เดียวกับ endpoint
- สิทธิ์
- credential ชนิด MCP ไม่ถือ scope — สิทธิ์มาจากแพ็กเกจเป็นราย function และการเรียกรายงานต้องเปิดสิทธิ์รายงานบน credential ใบนั้นอีกชั้น
- โควตาและบันทึก
- จำกัดได้ทั้งอัตราต่อนาทีและโควตาต่อวันแยกตามฟังก์ชันในแต่ละแพ็กเกจ · ทุกการเรียกถูกบันทึกและดูย้อนหลังได้ในแท็บของหน้านักพัฒนา
เครื่องมือที่เรียกได้
| get_report_overview | สรุปยอดซื้อขายภาพรวม ยอดขาย ยอดซื้อ และสินค้าขายดีตามช่วงเวลา สำหรับการเชื่อมต่อที่เปิดสิทธิ์รายงาน | รายงาน |
| get_document_report | รายงานตามประเภทเอกสาร จำนวนและยอดแยกตามประเภท เช่น ใบแจ้งหนี้ค้างชำระในเดือนนี้ | รายงาน |
| get_dashboard_counts | สรุปตัวเลขแดชบอร์ด ภาพรวมตัวเลขเบื้องต้นแบบย่อ เสริมรายงานฉบับเต็ม | รายงาน |
| list_documents | ค้นรายการเอกสาร กรองด้วยประเภท สถานะ และช่วงวันที่ แบบเดียวกับที่หน้าเอกสารใช้ | เอกสาร |
| get_document | อ่านเอกสารรายฉบับ รายการสินค้า ภาษี และ snapshot ของเอกสารที่ระบุ | เอกสาร |
| create_document_share_link | สร้างลิงก์แชร์เอกสาร ลิงก์อ่านเอกสารสำหรับคนนอกที่ไม่มีบัญชี ตั้ง PIN ได้ และหมดอายุเองหลังเปิดใช้ | เอกสาร |
| list_document_share_links | ดูลิงก์แชร์ของเอกสาร ลิงก์ทั้งหมดที่เคยสร้างให้เอกสารใบนี้ พร้อมสถานะและอายุที่เหลือ ไม่คืนโทเคน | เอกสาร |
| revoke_document_share_link | ปิดลิงก์แชร์ ตัดสิทธิ์อ่านของลิงก์ใบเดียว คนที่ถือ URL อยู่เปิดไม่ได้ทันที | เอกสาร |
| create_quotation_draft | ร่างใบเสนอราคา ผู้ช่วยเตรียมร่างใบเสนอราคาให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_invoice_draft | ร่างใบแจ้งหนี้ ผู้ช่วยเตรียมร่างใบแจ้งหนี้ให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_tax_invoice_draft | ร่างใบกำกับภาษี ผู้ช่วยเตรียมร่างใบกำกับภาษีให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_receipt_draft | ร่างใบเสร็จรับเงิน ผู้ช่วยเตรียมร่างใบเสร็จรับเงินให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_credit_note_draft | ร่างใบลดหนี้ ผู้ช่วยเตรียมร่างใบลดหนี้ให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_billing_note_draft | ร่างใบวางบิล ผู้ช่วยเตรียมร่างใบวางบิลให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_purchase_order_draft | ร่างใบสั่งซื้อ ผู้ช่วยเตรียมร่างใบสั่งซื้อให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_goods_receipt_draft | ร่างใบรับของ ผู้ช่วยเตรียมร่างใบรับของให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_delivery_note_draft | ร่างใบส่งของ ผู้ช่วยเตรียมร่างใบส่งของให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_material_request_draft | ร่างใบเบิกของ ผู้ช่วยเตรียมร่างใบเบิกของให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_expense_draft | ร่างใบบันทึกค่าใช้จ่าย ผู้ช่วยเตรียมร่างใบบันทึกค่าใช้จ่ายให้ คนยืนยันในแอปเพื่อออกเลขที่จริงเสมอ | เอกสาร |
| create_wht_cert_draft | ร่างหนังสือรับรองการหักภาษี ณ ที่จ่าย ร่างแบบ 50 ทวิ จากใบบันทึกค่าใช้จ่ายที่มีภาษีหัก ณ ที่จ่าย หรือกรอกรายการเงินได้เอง | เอกสาร |
| update_quotation | แก้ใบเสนอราคา แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบเสนอราคา | เอกสาร |
| update_invoice | แก้ใบแจ้งหนี้ แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบแจ้งหนี้ | เอกสาร |
| update_tax_invoice | แก้ใบกำกับภาษี แก้ได้เฉพาะฉบับร่างที่ไม่ได้ออกจากเอกสารต้นทาง และปฏิเสธ id ที่ไม่ใช่ใบกำกับภาษี | เอกสาร |
| update_receipt | แก้ใบเสร็จรับเงิน แก้ได้เฉพาะฉบับร่าง รวมถึงแก้รายการรับชำระ และปฏิเสธ id ที่ไม่ใช่ใบเสร็จ | เอกสาร |
| update_credit_note | แก้ใบลดหนี้ แก้ได้เฉพาะฉบับร่าง ตัวที่ออกจากใบกำกับภาษีแก้ได้แค่หมายเหตุ | เอกสาร |
| update_billing_note | แก้ใบวางบิล แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบวางบิล | เอกสาร |
| update_purchase_order | แก้ใบสั่งซื้อ แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบสั่งซื้อ | เอกสาร |
| update_goods_receipt | แก้ใบรับของ แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบรับของ | เอกสาร |
| update_delivery_note | แก้ใบส่งของ แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบส่งของ | เอกสาร |
| update_material_request | แก้ใบเบิกของ แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบเบิกของ | เอกสาร |
| update_expense | แก้ใบบันทึกค่าใช้จ่าย แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่ใบบันทึกค่าใช้จ่าย | เอกสาร |
| update_wht_cert | แก้หนังสือรับรองการหักภาษี ณ ที่จ่าย แก้ได้เฉพาะฉบับร่าง และปฏิเสธ id ที่ไม่ใช่หนังสือรับรองการหักภาษี | เอกสาร |
| update_document_status | ออกใบ รับชำระ ยกเลิก เดินสถานะเอกสาร — ออกเลขที่จริง บันทึกรับชำระ หรือยกเลิก | เอกสาร |
| search_clients | ค้นลูกค้าและผู้ขาย ค้นด้วยชื่อบางส่วน เลขผู้เสียภาษี หรือรหัสภายในของคุณ | ลูกค้าและผู้ขาย |
| create_client | เพิ่มลูกค้าหรือผู้ขาย สร้างรายชื่อใหม่ โดยเลขผู้เสียภาษีซ้ำกับรายเดิมไม่ได้ | ลูกค้าและผู้ขาย |
| list_products | ค้นรายการสินค้า กรองสินค้าด้วยชื่อหรือรหัส เพื่อใช้ประกอบการร่างเอกสาร | สินค้า |
| create_product | เพิ่มสินค้าหรือบริการ สร้างสินค้าใหม่ โดยรหัส SKU และบาร์โค้ดซ้ำกับของเดิมไม่ได้ | สินค้า |
| update_product | แก้สินค้า แก้ได้เฉพาะชื่อ จำนวนคงเหลือ และราคาขาย | สินค้า |
| get_company | บริบทบริษัทที่เชื่อมอยู่ ให้ผู้ช่วยรู้ว่ากำลังทำงานแทนบริษัทไหน ก่อนตอบเรื่องตัวเลข | บริษัท |