เอกสาร Open API

Ket Bill Open API v1 · ให้ระบบภายนอกและผู้ช่วย AI ทำงานแทนบริษัทของคุณ

openapi.json

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.

01

เริ่มต้นใช้งาน

Create a key, verify it against /me, then post your first draft document.

ขั้น 1

สร้างคีย์ API ในแอป

เข้า ketbill.com → ตั้งค่า → การเชื่อมต่อ → API Keys แล้วกดสร้างคีย์ เลือกเฉพาะ scope ที่ระบบของคุณต้องใช้จริง (หน้าสร้างคีย์มี scope ให้เลือกมากกว่าชุดที่ v1 ใช้)

คีย์ขึ้นต้นด้วย kdoc_ และแสดงครั้งเดียวตอนสร้าง เก็บลง secret store ทันที

ขั้น 2

ยิงคำขอครั้งแรกที่ /me

endpoint นี้ไม่ต้องมี scope ใช้ยืนยันว่าคีย์ใช้ได้และผูกกับบริษัทที่ถูกต้องก่อนเขียนโค้ดจริง

ได้ 401 INVALID_API_KEY = คีย์ผิดหรือถูกเพิกถอน · ได้ 403 OPEN_API_NOT_ENABLED = แพ็กเกจยังไม่เปิด

ขั้น 3

สร้างเอกสารฉบับร่างจากระบบของคุณ

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"
  }
}
02

การยืนยันตัวตน

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

    สร้าง OAuth app ในแอป

    ตั้งค่า → การเชื่อมต่อ → OAuth แล้วกดสร้าง ได้ client_id (และ client_secret ถ้าเลือกแบบ confidential) พร้อมตั้ง redirect URI ที่อนุญาต

  2. 2

    ส่งผู้ใช้ไป /oauth/authorize

    สร้าง code_verifier แบบสุ่ม ส่ง code_challenge = BASE64URL(SHA256(verifier)) ระบบพาผู้ใช้เข้าหน้า login ของ KetBill ตามปกติ ไม่มีหน้า consent แยก

  3. 3

    รับ code แล้วแลกเป็น token

    ผู้ใช้ล็อกอินเสร็จ ระบบ redirect กลับ redirect_uri พร้อม code แล้วคุณ POST /oauth/token ด้วย code + code_verifier

  4. 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=8f41c2

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"
  }'
03

สิทธิ์ (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: เท่านั้น ไม่ครอบลูกค้าหรือสินค้า ต้องการสองตระกูลให้เลือกทั้งสองฝั่ง

04

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 ผูกอยู่

เรียงล่าสุดก่อน · ไม่ส่ง includeTotal ค่า total เป็นตัวประมาณเพื่อความเร็ว (นับจากหน้าที่ดึงมา) ส่ง includeTotal=true เมื่อต้องการยอดรวมจริง · ยอดเงินทุกตัวเป็น string เพราะเป็น decimal ฝั่งฐานข้อมูล

paraminรายละเอียด
pagequeryหน้าที่ต้องการ เริ่มที่ 1
limitqueryจำนวนต่อหน้า ค่าปกติ 20 สูงสุด 100
qqueryค้นแบบมีคำนี้อยู่ข้างใน (contains) ใน 5 ช่อง: เลขที่เอกสาร, referenceCode, sourceOrderCode, ชื่อนิติบุคคลของลูกค้า และของคู่ค้า
typequeryประเภทเอกสาร ค่าทั้ง 10 ตัวอยู่ในตารางค่าอ้างอิง
statusqueryสถานะเอกสาร เทียบแบบตรงตัว ค่าทั้ง 13 ตัวอยู่ในตารางค่าอ้างอิง
sourceOrderCodequeryเลขออเดอร์ต้นทางจากระบบขาย เทียบแบบตรงตัวทั้งค่า ไม่ใช่บางส่วน — ใช้ถามว่า "ออเดอร์นี้ออกเอกสารไปแล้วหรือยัง" · ถ้าต้องการค้นบางส่วนของเลขออเดอร์ให้ใช้ q ซึ่งค้นคอลัมน์เดียวกันแบบ contains สองตัวนี้แทนกันไม่ได้
includeTotalquerytrue = นับยอดรวมจริง (ค่าอื่นถือว่าไม่ส่ง)
ส่งเข้า Playground

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)

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 เดิมได้ทันที ไม่มีเอกสารค้าง

paraminรายละเอียด
companyIdbodyต้องมีschema บังคับให้ส่ง แต่ค่าไม่มีผล — ระบบใช้บริษัทของ token เสมอ ไม่ส่งจะได้ 400
typebodyต้องมีประเภทเอกสารที่จะสร้าง
customerIdbodyลูกค้าปลายทาง (ใช้ vendorId สำหรับเอกสารฝั่งซื้อ)
issueDatebodyISO 8601 เต็มรูปเท่านั้น (2026-07-30T00:00:00.000Z) ส่งแบบวันที่ล้วนจะได้ 400 · ไม่ส่ง = วันนี้
linesbodyต้องมีอาร์เรย์รายการ ชื่อฟิลด์คือ lines · แต่ละบรรทัดต้องมี lineNo, description, quantity, unit, unitPrice, vatType ครบ
vatRatebodyอัตรา VAT ค่าปกติ 7
whtModebodyโหมดหัก ณ ที่จ่ายของเอกสาร: NONE, DOCUMENT_LEVEL, LINE_LEVEL
ส่งเข้า Playground

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}เอกสารรายฉบับ พร้อมรายการและ snapshot

คืนค่า snapshot ที่ล็อกไว้ตอนออกเอกสาร ไม่ใช่ข้อมูล master ปัจจุบัน · ตัวอย่างด้านล่างตัดมาบางฟิลด์ ของจริงคืน lines, payments, parentRefs/childRefs, issuerSnapshot/partySnapshot ครบทั้งใบ

paraminรายละเอียด
idpathต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
ส่งเข้า Playground

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}แก้ไขเอกสารที่ยังแก้ได้

สามกรณีที่ยังติด 409 แม้เอกสารเป็นฉบับร่าง

  • ใบกำกับภาษีที่ออกจากใบแจ้งหนี้หรือใบเสร็จ — TAX_INVOICE_SOURCE_LOCKED ต้องแก้ที่เอกสารต้นทาง
  • ใบลดหนี้ที่อ้างใบกำกับภาษี — ส่งได้แค่ remark กับ internalNote ฟิลด์อื่นตอบ CREDIT_NOTE_SOURCE_LOCKED
  • มีคนเปิดแก้ใบนั้นอยู่ในแอป — LOCK_HELD เส้นนี้ไม่ต้องขอ edit lock เอง แต่ทับคนที่ถืออยู่ไม่ได้

ส่งมาเฉพาะฟิลด์ที่ต้องการแก้ ฟิลด์ที่ไม่ส่งคงค่าเดิม · แก้ได้เฉพาะฉบับร่าง สถานะอื่นตอบ 409 พร้อม code บอกเหตุผล · ส่ง lines มาคือแทนที่รายการทั้งชุด ไม่ใช่การเพิ่มบรรทัด · ยอดเงินคำนวณใหม่ให้จาก lines ที่ส่งมา

paraminรายละเอียด
idpathต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
remark, internalNotebodyหมายเหตุบนเอกสาร และโน้ตภายในที่ไม่พิมพ์ออกเอกสาร
issueDate, dueDatebodyISO 8601 เต็มรูป · dueDate ส่ง null เพื่อล้างกำหนดชำระ ส่วน issueDate ล้างไม่ได้
customerId, vendorIdbodyเปลี่ยนคู่ค้าของเอกสาร ส่ง null เพื่อถอดออก
linesbodyแทนที่รายการทั้งชุด แต่ละบรรทัดต้องมี lineNo, description, quantity, unit, unitPrice, vatType ครบเหมือนตอนสร้าง
whtMode, whtRate, whtRateIdbodyโหมดและอัตราหักภาษี ณ ที่จ่าย ดูค่าที่รับได้ในตารางค่าอ้างอิง
vatInclusivebodytrue = ราคาที่ส่งมารวม VAT แล้ว
paymentTerms, paymentMethod, receiptPaymentbodyเงื่อนไขและวิธีชำระ กับข้อมูลการรับชำระบนใบเสร็จ
referenceCodebodyรหัสอ้างอิงของคุณเอง ใช้ค้นด้วย q ได้
ส่งเข้า Playground

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}ลบเอกสารฉบับร่าง

ลบแบบ soft-delete และทำได้เฉพาะฉบับร่างตามนโยบายของ service — ออกเอกสารไปแล้วให้ใช้ยกเลิกแทน · สำเร็จตอบ 204 ไม่มีเนื้อหา · ลบไปแล้ว 409 ALREADY_DELETED · ฉบับร่างที่มีคนกำลังแก้ในแอป 409 LOCK_HELD

paraminรายละเอียด
idpathต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
ส่งเข้า Playground

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}สถานะงานสร้างเอกสารที่เข้าคิวไว้

สถานะไต่ 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

paraminรายละเอียด
jobIdpathต้องมีjob id ที่ได้จาก job_id หรือ header Location ตอน POST /documents ตอบ 202
ส่งเข้า Playground

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สร้างเอกสารลูกจากเอกสารนี้

สร้างเอกสารใหม่ตาม childType โดยยกคู่ค้าและบรรทัดมาจากต้นทาง — เอกสารต้นทางไม่ถูกแก้เนื้อหา · ตอบ 201 พร้อมเอกสารลูกฉบับร่าง และ parentRefs ชี้กลับไปที่ต้นทาง · สถานะต้นทางยังแปลงไม่ได้ 409 DOCUMENT_CONVERT_NOT_ALLOWED · ใบแจ้งหนี้/ใบวางบิลที่ยังไม่รับชำระแปลงเป็นใบเสร็จไม่ได้ 409 DOCUMENT_CONVERT_PAYMENT_REQUIRED · บริษัทที่ยังไม่จดทะเบียน VAT ออกใบกำกับภาษีไม่ได้ 403

paraminรายละเอียด
idpathต้องมีid ของเอกสารต้นทาง ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
childTypebodyต้องมีชนิดเอกสารลูกที่จะสร้าง ค่าทั้ง 10 ตัวอยู่ในตารางค่าอ้างอิง
lineSelectionsbodyเลือกบรรทัดและจำนวนที่ยกไปเอกสารลูก ไม่ส่ง = ยกทั้งฉบับ
issueDatebodyวันที่ของเอกสารลูก ISO 8601 เต็มรูป ไม่ส่ง = วันนี้
ส่งเข้า Playground

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ออกเอกสารจากฉบับร่าง

เปลี่ยนสถานะจากร่างเป็นออกแล้ว/รอรับชำระตามชนิดเอกสาร · เลขที่เอกสารมีตั้งแต่ตอนสร้างและไม่เปลี่ยนตอนออก · ออกได้เฉพาะฉบับร่าง — ออกไปแล้ว 409 ALREADY_ISSUED สถานะอื่น 409 ISSUE_NOT_ALLOWED · ใบกำกับภาษีแบบตรงต้องแนบ receiptPayment ให้ครบ ไม่ครบ 409 TAX_INVOICE_PAYMENT_* · มีคนเปิดแก้ใบนั้นในแอป 409 LOCK_HELD · ส่ง header Idempotency-Key ได้เพื่อกันออกซ้ำ

paraminรายละเอียด
idpathต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
receiptPaymentbodyข้อมูลการรับชำระที่ต้องแนบเมื่อออกใบกำกับภาษีแบบตรง — เอกสารชนิดอื่นไม่ต้องส่ง
ส่งเข้า Playground

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บันทึกรับชำระของเอกสารที่ออกแล้ว

บันทึกยอดที่รับจริง — ยอดเต็มปิดเป็นรับชำระแล้ว ยอดบางส่วนคงสถานะรับชำระบางส่วน · รายการที่บันทึกโผล่ใน payments ของเอกสาร · ทำได้เฉพาะเอกสารที่รอรับชำระ สถานะอื่น 409 MARK_PAID_NOT_ALLOWED · ยอดเกินคงเหลือ 409 · ฉบับร่างยังบันทึกไม่ได้ ต้องออกเอกสารก่อน

paraminรายละเอียด
idpathต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
amountbodyต้องมียอดที่รับจริง ห้ามเกินยอดคงเหลือของเอกสาร
paidDatebodyวันที่รับชำระ ISO 8601 เต็มรูป ไม่ส่ง = วันนี้
methodbodyวิธีชำระ เป็นข้อความอิสระ ไม่ใช่ enum
bankRefbodyเลขอ้างอิงจากธนาคาร
notebodyหมายเหตุของรายการชำระ
ส่งเข้า Playground

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ยกเลิกเอกสารพร้อมเอกสารที่สืบต่อ

ยกเลิกแล้วลามถึงเอกสารลูกด้วย ไม่ใช่แค่ฉบับที่ระบุ

  • voidSummary.cancelledDocumentIds บอกครบว่าฉบับไหนถูกยกเลิกไปบ้าง — อ่านก่อนแจ้งผู้ใช้
  • เอกสารลูกที่รับชำระแล้วก็ถูกยกเลิกไปด้วยเมื่อ policy สั่งให้ลาม
  • ยกเลิกแล้วย้อนกลับไม่ได้ ต้องออกเอกสารใหม่แทน

ยกเลิกเอกสารและเอกสารที่สืบต่อจากมันตามนโยบายของระบบ · voidSummary บอกจำนวนและ id ที่ถูกยกเลิกจริง · ยกเลิกไม่ได้ตามนโยบาย 409 พร้อม code จาก void-policy · ฉบับร่างที่มีคนกำลังแก้ในแอป 409 LOCK_HELD

paraminรายละเอียด
idpathต้องมีid ของเอกสาร ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
reasonbodyเหตุผลที่ยกเลิก เก็บลง voidedReason ของเอกสาร
ส่งเข้า Playground

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รายการลูกค้าและผู้ขาย

endpoint นี้คืนอาร์เรย์ตรง ๆ ไม่มี total/page/limit ห่อไว้ — อ่านผลจาก response ทั้งก้อน ไม่ใช่ res.items · แต่ละแถวพ่วง addresses, clientPersons และ accountManager มาด้วย (ตัวอย่างตัดมาบางฟิลด์)

paraminรายละเอียด
pagequeryหน้าที่ต้องการ เริ่มที่ 1
limitqueryจำนวนต่อหน้า ค่าปกติ 100 สูงสุด 200
qqueryค้นชื่อจดทะเบียน ชื่อเรียก เลขผู้เสียภาษี รหัสภายใน อีเมล เบอร์โทร รวมถึงชื่อผู้ติดต่อ
kindqueryCUSTOMER, VENDOR หรือ BOTH · กรอง CUSTOMER แล้วแถว BOTH ติดมาด้วยเสมอ
ส่งเข้า Playground

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 เดิม

ส่ง externalId ของระบบต้นทางมาด้วย — ถ้าเคยส่งแล้วระบบอัปเดตรายเดิมและตอบ 200 ถ้าเป็นรายใหม่ตอบ 201 (ยิงซ้ำได้ไม่เกิดข้อมูลซ้ำ) · ฟิลด์ที่ไม่มีใน schema ถูกตัดทิ้งเงียบ ๆ ไม่มี error เหมือนเส้นสร้างเอกสาร

paraminรายละเอียด
kindbodyต้องมีCUSTOMER, VENDOR หรือ BOTH — ไม่มีค่าปกติ ไม่ส่งจะได้ 400
legalNamebodyต้องมีชื่อตามหนังสือรับรอง
taxIdbodyเลขผู้เสียภาษี 13 หลัก
externalIdbodyid ของลูกค้ารายนี้ในระบบต้นทาง
addressesbodyที่อยู่จดทะเบียนและที่อยู่จัดส่ง
ส่งเข้า Playground

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}แก้ไขลูกค้าด้วย id

addresses กับ clientPersons เป็นการแทนที่ทั้งชุด ไม่ใช่การเพิ่มรายการ

  • ส่งมาเมื่อไหร่ ของเดิมถูกลบทิ้งทั้งหมดก่อนเขียนชุดใหม่ — แถวใหม่ได้ id ใหม่ อ้าง id เดิมต่อไม่ได้
  • ไม่ส่งฟิลด์นั้นมาเลย = คงของเดิมไว้ครบ · ส่งอาเรย์ว่างคือสั่งลบให้เหลือศูนย์รายการ
  • ที่อยู่มีที่อยู่หลักได้ชุดละหนึ่งรายการ — ไม่ระบุ isPrimary ระบบตั้งรายการแรกให้ ระบุมาหลายรายการเหลือรายการแรกที่ระบุ

เส้นนี้อ้าง id เท่านั้น ไม่ใช่ upsert ตาม externalId เหมือน POST · ส่งมาเฉพาะฟิลด์ที่ต้องการแก้ ฟิลด์ที่ไม่ส่งคงค่าเดิม · ฟิลด์ที่ไม่มีใน schema ถูกตัดทิ้งเงียบ ๆ ไม่มี error เหมือนเส้นสร้างลูกค้า · ไม่พบ คนละบริษัท หรือถูกลบไปแล้ว ตอบ 404 NOT_FOUND

paraminรายละเอียด
idpathต้องมีid ของลูกค้า ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
kindbodyCUSTOMER, VENDOR หรือ BOTH
legalName, displayNamebodyชื่อตามหนังสือรับรอง และชื่อที่ใช้เรียกสั้น ๆ
taxId, branchCodebodyเลขผู้เสียภาษี 13 หลัก และรหัสสาขา 5 หลัก
phone, emailbodyส่ง null เพื่อล้างค่าเดิม
externalIdbodyid ของลูกค้ารายนี้ในระบบต้นทาง แก้ได้ แต่เส้นนี้ไม่ได้ใช้มันค้นหา
isActivebodyfalse = ปิดใช้งาน ไม่ใช่การลบ ลูกค้ายังอยู่ในรายการ
addressesbodyแทนที่ที่อยู่ทั้งชุด ดูคำเตือนด้านบน
clientPersonsbodyแทนที่ผู้ติดต่อทั้งชุด ดูคำเตือนด้านบน
ส่งเข้า Playground

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รายการสินค้าและบริการ

ใช้ดึงราคาและหน่วยล่าสุดก่อนสร้างเอกสาร · คืนอาร์เรย์ตรง ๆ ไม่มี total/page/limit ห่อไว้ เหมือน /clients · ราคาเป็น string เพราะเป็น decimal ฝั่งฐานข้อมูล ส่วน stock เป็นตัวเลข

paraminรายละเอียด
pagequeryหน้าที่ต้องการ เริ่มที่ 1
limitqueryจำนวนต่อหน้า ค่าปกติ 100 สูงสุด 100
qqueryค้นชื่อสินค้า SKU บาร์โค้ด หรือคำอธิบาย
typequeryGOODS หรือ SERVICE
ส่งเข้า Playground

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 เดิม

รูปแบบ upsert เดียวกับลูกค้า — externalId ซ้ำคืออัปเดต ไม่ใช่สร้างใหม่ · เส้นนี้เข้มกว่า: body รับเฉพาะฟิลด์ที่ประกาศไว้ ส่งฟิลด์แปลกปลอมมาจะได้ 400 (ต่างจากเส้นสร้างเอกสารและสร้างลูกค้าที่ตัดทิ้งเงียบ ๆ)

paraminรายละเอียด
namebodyต้องมีชื่อสินค้าหรือบริการ — ฟิลด์เดียวที่บังคับ
unitbodyหน่วยนับที่จะขึ้นบนเอกสาร · ไม่ส่ง = ชิ้น
defaultPricebodyราคาตั้งต้นต่อหน่วย · ไม่ส่ง = 0
skubodyรหัสสินค้าของคุณ
vatTypebodyVAT_7, VAT_0 หรือ NON_VAT · ไม่ส่ง = ตามสถานะจดทะเบียน VAT ของบริษัท
ส่งเข้า Playground

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}สินค้ารายตัว

ไม่พบสินค้าในบริษัทของ token จะได้ 404 NOT_FOUND

paraminรายละเอียด
idpathต้องมีid ของสินค้า ต้องเป็นจำนวนเต็มตั้งแต่ 1 ขึ้นไป ไม่ใช่จะได้ 422
ส่งเข้า Playground

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 ใบนี้ผูกอยู่

ไม่ต้องมี scope — เรียกครั้งแรกด้วย endpoint นี้เพื่อยืนยันว่าคีย์ใช้ได้และผูกกับบริษัทที่ถูกต้อง

ส่งเข้า Playground

GET /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 endpoints

authorization-code + PKCE สำหรับแอปที่ออก token แทนผู้ใช้

GET/oauth/authorizeจุดเริ่ม flow — พาผู้ใช้เข้าหน้า login ของ KetBill

เปิด URL นี้ใน browser ของผู้ใช้ ไม่ใช่เรียกจาก server · ระบบพาผู้ใช้เข้าหน้า login ปกติของแอป (flow เดียวกับการเชื่อมต่อจาก Ketshopweb) ผู้ใช้ล็อกอินและเลือกบริษัทตามปกติ แล้ว redirect กลับ redirect_uri พร้อม ?code=…&state=…

paraminรายละเอียด
response_typequeryต้องมีcode
client_idqueryต้องมีclient_id ของ OAuth app ที่สร้างในแอป
redirect_uriqueryต้องมีต้องตรงกับที่ตั้งไว้ใน OAuth app
code_challengequeryต้องมีBASE64URL(SHA256(code_verifier))
code_challenge_methodqueryต้องมีS256
scopequeryscope ที่ขอ คั่นด้วยช่องว่าง
statequeryค่าสุ่มที่คุณตรวจตอน 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=8f41c2

response

HTTP/1.1 302 Found
Location: https://partner.example.com/callback?code=ac_3nQ…&state=8f41c2
POST/oauth/tokenแลก code เป็น access token

confidential client ส่ง client_secret ใน body (client_secret_post) ไม่ใช่ header Basic · access token ไม่มีวันหมดอายุ จึงไม่มี refresh token

paraminรายละเอียด
grant_typebodyต้องมีauthorization_code
codebodyต้องมีcode ที่ได้จากขั้น authorize
code_verifierbodyต้องมีค่าจริงของ code_challenge
client_idbodyต้องมีclient_id ของ OAuth app
client_secretbodyเฉพาะ 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)

ลบ per-user salt ของ token นั้น ทำให้ access token ทุกใบของผู้ใช้รายนั้นใช้ไม่ได้ทันที · ตอบ 200 เสมอ แม้ token ไม่มีอยู่

paraminรายละเอียด
tokenbodyต้องมี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 OK
GET/.well-known/jwks.jsonpublic key สำหรับ verify token แบบ offline

จับคู่ 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)

ให้ 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)

บอกว่า /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 endpoints

GET/v1/payment-accountsบัญชีรับเงินของบริษัทที่ token ผูกอยู่

มีไว้ให้ทำ dropdown เลือกบัญชี แทนการให้ผู้ใช้พิมพ์ id เอง · คืนทั้งหมดครั้งเดียว ไม่แบ่งหน้า (บัญชีรับเงินต่อบริษัทมีหลักหน่วย) เรียงตามลำดับที่สร้างไว้ในหน้าตั้งค่า · บริษัทที่ยังไม่ได้ตั้งบัญชีได้ items เป็น array ว่าง ไม่ใช่ 404 · บัญชีที่ปิดใช้งานแล้วยังคืนมาด้วย isActive: false เพื่อให้ค่าที่ผูกไว้เดิมยังแสดงชื่อได้ — กรองเองถ้าไม่อยากให้เลือกใหม่ · สองบัญชีในชื่อนิติบุคคลเดียวกันจะเห็นเป็นสองแถวชื่อซ้ำ ให้แยกด้วย bankName กับ accountNumber

ส่งเข้า Playground

GET /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 endpoints

GET/v1/eventsประวัติ event ของบริษัท เรียงใหม่ไปเก่า

ไล่หน้าด้วย cursor ไม่ใช่ page — ส่ง next_cursor ที่ได้กลับไปที่ query cursor เพื่อขอหน้าถัดไป · แต่ละรายการแนบผลการส่งเข้า webhook ทุก endpoint ไว้ใน deliveries · คีย์เก่าที่มี scope documents:read หรือ documents:* ก็ยังเรียกได้ (เผื่อความเข้ากันได้กับคีย์ที่ออกก่อนมี events:read)

paraminรายละเอียด
typequerydocument.created หรือ document.failed — ไม่ส่ง = ทุกชนิด
cursorqueryid ของ event ตัวสุดท้ายที่ได้จากหน้าก่อน ไม่ส่ง = เริ่มจากล่าสุด
limitqueryจำนวนต่อหน้า ค่าปกติ 20 สูงสุด 100
ส่งเข้า Playground

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 รายตัว พร้อมประวัติการส่งรายครั้ง

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)

paraminรายละเอียด
idpathต้องมีevent id (evt_...) จาก GET /v1/events หรือจาก payload ที่ยิงเข้า webhook
ส่งเข้า Playground

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 อีกครั้ง

ตอบ 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 เพียงอย่างเดียวไม่พอ)

paraminรายละเอียด
idpathต้องมีevent id (evt_...) ที่จะส่งซ้ำ
endpoint_idbodyส่งเฉพาะ endpoint นี้ ไม่ส่ง = ส่งซ้ำทุก endpoint ที่สมัคร event ชนิดนี้
ส่งเข้า Playground

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 endpoints

GET/v1/webhooksรายการ webhook endpoint ของบริษัท

secret ไม่ถูกคืนอีกหลังตอนสร้าง เหลือแค่ secret_preview · จำกัดสูงสุด 5 endpoint ต่อบริษัท · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)

ส่งเข้า Playground

GET /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 ใหม่

จำกัด 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 เพียงอย่างเดียวไม่พอ)

paraminรายละเอียด
urlbodyต้องมีปลายทาง ต้องเป็น https และเป็นที่อยู่สาธารณะเท่านั้น
eventsbodyต้องมีarray อย่างน้อย 1 ตัว ค่าที่รับได้: document.created, document.failed
descriptionbodyป้ายกำกับช่วยจำ ไม่กระทบการทำงาน
ส่งเข้า Playground

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 หรือเปิดใช้งานอีกครั้ง

ส่งมาเฉพาะฟิลด์ที่ต้องการแก้ ฟิลด์ที่ไม่ส่งคงค่าเดิม · ส่ง status=ACTIVE เพื่อเปิดใช้งาน endpoint ที่ถูกปิดอัตโนมัติ (ล้างนาฬิกา 24 ชั่วโมงให้ด้วย) · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)

paraminรายละเอียด
idpathต้องมีwebhook endpoint id (whe_...) ต้องเป็นของบริษัทเดียวกับ token
urlbodyเปลี่ยนปลายทาง ต้องเป็น https และเป็นที่อยู่สาธารณะเหมือนตอนสร้าง
eventsbodyแทนที่ชุด event ทั้งหมด ไม่ใช่การเพิ่ม
descriptionbodyส่ง null เพื่อลบป้ายกำกับ
statusbodyส่งได้ค่าเดียวคือ ACTIVE ใช้เปิดใช้งาน endpoint ที่ถูกปิดอัตโนมัติ
ส่งเข้า Playground

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 endpoint

ลบแล้วเหตุการณ์ใหม่ไม่ถูกส่งมาที่นี่อีก และรายการที่ค้างรอส่งซ้ำถูกยกเลิก · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)

paraminรายละเอียด
idpathต้องมีwebhook endpoint id (whe_...) ที่จะลบ
ส่งเข้า Playground

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 นี้

ส่ง payload ตัวอย่าง type=ping ที่ลงลายเซ็นจริงด้วย secret ของ endpoint นั้น เพื่อตรวจว่าปลายทางรับได้ · ไม่ถูกนับเป็นประวัติการส่งจริงและไม่กระทบตัวนับที่ใช้ปิดอัตโนมัติ · คีย์เก่าที่มี scope documents:* ก็ยังเรียกได้ (documents:read เพียงอย่างเดียวไม่พอ)

paraminรายละเอียด
idpathต้องมีwebhook endpoint id (whe_...) ที่จะยิงทดสอบ
ส่งเข้า Playground

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
}
05

ตารางค่าอ้างอิง

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
06

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 ให้ดูเลขสถานะประกอบ

httpcodeความหมาย
400VALIDATIONฟิลด์บังคับขาด หรือส่งฟิลด์ที่ไม่มีใน schema มาที่เส้นที่ไม่รับฟิลด์เกิน · มี issues บอกชื่อฟิลด์ที่ผิดมาด้วย
401AUTH_REQUIREDไม่ได้แนบ token มาใน header
401INVALID_API_KEYtoken ผิด หมดอายุ หรือถูกเพิกถอนแล้ว
403OPEN_API_NOT_ENABLEDแพ็กเกจของบริษัทยังไม่เปิด Open API
403SCOPE_FORBIDDENtoken ไม่มี scope ที่ endpoint นี้ต้องใช้
404NOT_FOUNDไม่พบข้อมูลในบริษัทของ token
422VALIDATIONชนิดข้อมูลผิดที่ขอบ เช่น id ไม่ใช่จำนวนเต็ม หรือ boolean ส่งมาเป็นข้อความ · message เป็นรายละเอียดจากตัวตรวจ schema
429RATE_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มีคนเปิดแก้เอกสารนี้ในแอปอยู่ รอให้ปิดหรือหมดเวลาแล้วลองใหม่
07

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

บริบทบริษัทที่เชื่อมอยู่

ให้ผู้ช่วยรู้ว่ากำลังทำงานแทนบริษัทไหน ก่อนตอบเรื่องตัวเลข

บริษัท