API reference ของ Liteo
เชื่อม Pabbly หรือ Zapier กับ Liteo ด้วย API key ของเวิร์กสเปซ หรือเชื่อม AI client ผ่าน MCP
ภาพรวมของ API
API นี้ใช้เชื่อม Pabbly, Zapier, AI client ผ่าน MCP หรือระบบที่เขียนเองเข้ากับเวิร์กสเปซ Liteo ได้ครั้งละ 1 เวิร์กสเปซ หน้านี้อธิบายเรื่อง authentication, polling, endpoint สำหรับอ่านและเขียน record ที่ Zapier ใช้ และ MCP server
- Base URL:
https://elated-rat-401.convex.site/v1 - Auth: Bearer token (โค้ดเชื่อมต่อ)
- Format: JSON
ยืนยันตัวตนด้วยโค้ดเชื่อมต่อ
ใน Liteo ให้เปิด Settings แล้วไปที่ Integrations เลือก Pabbly หรือ Zapier แล้วสร้างการเชื่อมต่อ พอ Liteo แสดงโค้ดเชื่อมต่อ ก็คัดลอกเก็บไว้ทันที เปิดหน้า Integrations ใน Liteo
ส่งโค้ดนี้เป็น Bearer token ไปกับทุก request และเก็บไว้เป็นความลับ เพราะ Liteo แสดงให้ดูแค่ครั้งเดียว
Authorization: Bearer YOUR_CONNECTION_CODEโค้ดเชื่อมต่อคือ API key ของเวิร์กสเปซ ใช้อ่าน record ได้ และใช้แก้ข้อมูลผ่าน endpoint สำหรับเขียนด้านล่างได้ด้วย ส่วน endpoint polling ของ Pabbly อ่านได้อย่างเดียว
แอดมินเป็นคนสร้างการเชื่อมต่อของเวิร์กสเปซ และเลือกคนดูแลเริ่มต้นให้ record ใหม่ ให้เก็บ key ไว้บนเซิร์ฟเวอร์ของตัวเอง หรือในที่เก็บ credential ของ integration และห้ามใส่ใน URL หรือหน้าเว็บสาธารณะ
ถ้าจะเปลี่ยน key ให้สร้างอันใหม่ก่อน แล้วแก้ workflow ให้ใช้อันใหม่ จากนั้นค่อยยกเลิก (revoke) key เก่าที่ Settings → Integrations พอยกเลิกแล้ว request ที่ใช้ key นั้นจะใช้ไม่ได้อีก แต่ข้อมูล CRM ที่มีอยู่แล้วยังอยู่ครบ
ทดสอบการเชื่อมต่อ
GET /v1/me
ใช้เช็กว่าโค้ดเชื่อมต่อใช้งานได้ไหม และดูว่าโค้ดนี้เข้าถึงเวิร์กสเปซไหนได้
curl "https://elated-rat-401.convex.site/v1/me" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE"{
"keyName": "Pabbly",
"scope": "service",
"workspaceName": "Your workspace"
}ดึงผู้ติดต่อใหม่
GET /v1/pabbly/contacts
ส่งผู้ติดต่อกลับมาโดยเรียงใหม่สุดก่อน และ response จะเป็น array ชื่อ items เพื่อให้ Pabbly ตรวจผู้ติดต่อแต่ละรายเป็น record แยกกันได้
ใส่ ?limit=100 เพื่อขอสูงสุด 100 ราย ถ้าไม่ใส่จะได้ 25 ราย
curl "https://elated-rat-401.convex.site/v1/pabbly/contacts?limit=100" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE"{
"items": [
{
"id": "contact_123",
"createdAt": 1788804000000,
"firstName": "Ada",
"lastName": "Lovelace",
"workEmail": "ada@example.com",
"workPhone": "+1 555 0100",
"jobTitle": "Founder",
"type": "lead",
"businessId": "",
"tagIds": "",
"customFields": "{}",
"assignedToId": "user_123",
"needsReview": false
}
],
"nextCursor": null
}| Field | ความหมาย |
|---|---|
id | ID ของผู้ติดต่อใน Liteo ที่ไม่ซ้ำกับรายอื่น |
createdAt | เวลาที่สร้างผู้ติดต่อ เป็น Unix timestamp หน่วยมิลลิวินาที |
firstName | ชื่อ |
lastName | นามสกุล |
workEmail | อีเมลที่ทำงาน |
workPhone | เบอร์โทรที่ทำงาน |
jobTitle | ตำแหน่งงาน |
type | lead หรือ customer |
businessId | ID ของบริษัทที่เชื่อมอยู่ หรือสตริงว่าง |
tagIds | ID ของแท็ก คั่นด้วยเครื่องหมายคอมมา |
customFields | ฟิลด์ที่สร้างเอง ในรูป JSON string |
assignedToId | ID ของสมาชิก Liteo ที่เป็นคนดูแลผู้ติดต่อรายนี้ |
previousOwnerId | ID ของคนดูแลคนก่อน หรือสตริงว่าง |
needsReview | ผู้ติดต่อรายนี้ต้องตรวจสอบหรือไม่ |
ดึงดีลใหม่
GET /v1/pabbly/deals
ส่งดีลกลับมาใน array items โดยเรียงใหม่สุดก่อน ใช้โค้ดเชื่อมต่อตัวเดียวกัน และใส่ limit ได้เหมือน endpoint ของผู้ติดต่อ
curl "https://elated-rat-401.convex.site/v1/pabbly/deals?limit=100" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE"{
"items": [{
"id": "deal_123",
"name": "Website project",
"value": 2500,
"currency": "USD",
"stageId": "stage_123",
"stageName": "Proposal",
"stageColor": "",
"pipelineId": "pipeline_123",
"pipelineName": "Sales",
"nextFollowUp": "2026-09-15",
"businessId": "",
"businessName": "",
"primaryContactName": "",
"closedAt": 0,
"lostReason": "",
"stageMovedAt": 1788804000000
}],
"nextCursor": null
}| Field | ความหมาย |
|---|---|
id | ID ของดีลใน Liteo ที่ไม่ซ้ำกับดีลอื่น |
name | ชื่อดีล |
value | มูลค่าดีล |
currency | รหัสสกุลเงิน 3 ตัวอักษร |
stageId | ID ของขั้นในไปป์ไลน์ |
stageName | ชื่อขั้นในไปป์ไลน์ |
stageColor | สีของขั้นในไปป์ไลน์ หรือสตริงว่าง |
pipelineId | ID ของไปป์ไลน์ |
pipelineName | ชื่อไปป์ไลน์ |
nextFollowUp | วันที่ต้องตามต่อ ในรูป YYYY-MM-DD หรือสตริงว่าง |
businessId | ID ของบริษัทที่เชื่อมอยู่ หรือสตริงว่าง |
businessName | ชื่อบริษัทที่เชื่อมอยู่ หรือสตริงว่าง |
primaryContactName | ชื่อผู้ติดต่อหลัก หรือสตริงว่าง |
closedAt | เวลาที่ปิดดีล เป็น Unix timestamp หน่วยมิลลิวินาที หรือ 0 |
lostReason | เหตุผลที่แพ้ดีล หรือสตริงว่าง |
stageMovedAt | เวลาที่ย้ายขั้นล่าสุด เป็น Unix timestamp หน่วยมิลลิวินาที หรือ 0 |
ข้อจำกัดของ polling
ใน Pabbly ให้อ่าน array items แล้วใช้ id ระบุ record แต่ละรายการ ให้ใช้ ID นี้ตามที่ได้มาโดยไม่ต้องแก้ทุกรอบที่เช็ก record หนึ่งรายการจะได้เริ่ม workflow แค่ครั้งเดียว
ตั้ง limit=100 จะได้ช่วง polling กว้างที่สุดของ Pabbly แต่การเช็กแต่ละรอบก็ได้ไม่เกิน 100 record ถ้าระหว่างสองรอบมี record เข้ามาเกิน 100 รายการ record ที่เก่ากว่าอาจหลุดไป
endpoint ของ Pabbly ทั้งสองตัวส่ง nextCursor กลับมา ถ้ายังมี record ที่เก่ากว่า และส่ง null ถ้าไม่เหลือแล้ว ให้ส่งค่านี้เป็น cursor ใน request ถัดไป connector ต้องตามหน้าถัดไปเอง เพราะ step polling ของ Pabbly อาจอ่านแค่หน้าแรก
connector ของ Zapier เช็ก record ล่าสุดได้สูงสุด 1,000 รายการต่อ poll โดยดึงทีละหน้า หน้าละ 100 ถ้าระหว่างสองรอบมี record เข้ามาเกิน 1,000 รายการ record ที่เก่ากว่าอาจหลุดไป และ Zapier อาจพักชุดข้อมูลก้อนใหญ่ไว้ให้ยืนยันก่อน
แผนของ Pabbly หรือ Zapier ที่ใช้อยู่เป็นตัวกำหนดว่าจะเช็กถี่แค่ไหน ก่อนจะพึ่ง polling ควรลองกับปริมาณข้อมูลจริงที่คาดว่าจะเจอก่อน ถ้าต้องการนำเข้าหรือกู้ข้อมูลให้ครบ ให้ใช้ endpoint แบบแบ่งหน้าด้านล่าง และเก็บ ID ของ record ที่ประมวลผลแล้วไว้ด้วย
ดึงรายการและค้นหา record
endpoint กลุ่มนี้ใช้กับ Zapier และ integration ที่เขียนเอง และส่ง data กับ nextCursor กลับมา ผู้ติดต่อและดีลเรียงใหม่สุดก่อน ให้ใช้ _id ของ record แต่ละรายการเป็น ID ที่ไม่เปลี่ยน
| Endpoint | ความหมาย |
|---|---|
GET /v1/contacts | ดึงรายชื่อผู้ติดต่อ ใส่ email เพื่อค้นหาอีเมลแบบตรงตัว โดยไม่แยกตัวพิมพ์เล็กพิมพ์ใหญ่ |
GET /v1/deals | ดึงรายการดีล พร้อมรายละเอียดของไปป์ไลน์และขั้น |
GET /v1/businesses | ดึงรายการบริษัท |
GET /v1/tasks | ดึงรายการงาน |
GET /v1/pipelines | ดึงไปป์ไลน์พร้อมขั้นทั้งหมด ใช้ ID เหล่านี้ตอนสร้างดีล |
GET /v1/pipelines/{id}/stages | ดึงขั้นทั้งหมดของไปป์ไลน์เดียว เรียงตามลำดับขั้น |
GET /v1/tags | ดึงแท็กที่มีอยู่แล้ว ตอนติดแท็กให้ผู้ติดต่อ ให้ใช้ ID หรือชื่อที่ตรงเป๊ะ |
GET /v1/members | ดึงสมาชิกของเวิร์กสเปซที่ยัง active ไว้ใช้มอบหมาย record ให้สมาชิก |
GET /v1/contacts/{id} | อ่านผู้ติดต่อ 1 ราย |
GET /v1/businesses/{id} | อ่านบริษัท 1 แห่ง |
GET /v1/deals/{id} | อ่านดีล 1 ดีล |
request แบบ list รับ limit ตั้งแต่ 1 ถึง 100 (ค่าเริ่มต้น 25) และ cursor (ไม่ใส่ก็ได้) ส่ง nextCursor ที่ได้ไปใน request ถัดไปโดยไม่ต้องแก้ แล้วหยุดเมื่อ nextCursor เป็น null
list ของสมาชิกและของขั้นจะส่ง _id กับ name ของแต่ละตัวเลือกกลับมา ให้ส่ง _id ของสมาชิกไปเป็น assignedToId โดยไม่ต้องแก้ หน้าของสมาชิกอาจว่างเปล่าทั้งที่ nextCursor ยังชี้ไปหาสมาชิกคนอื่นอยู่ ก็ให้ตาม cursor ต่อไปเรื่อย ๆ จนกว่าจะเป็น null
curl --get "https://elated-rat-401.convex.site/v1/contacts" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE" \
--data-urlencode "email=ada@example.com" \
--data-urlencode "limit=100"{
"data": [{
"_id": "contact_123",
"_creationTime": 1788804000000,
"firstName": "Ada",
"lastName": "Lovelace",
"workEmail": "ada@example.com",
"workPhone": "+1 555 0100",
"jobTitle": "Founder",
"type": "lead",
"businessId": null,
"tags": [],
"customFields": {},
"assignedToId": "member_123"
}],
"nextCursor": null
}ตัวอย่างนี้แสดงเฉพาะฟิลด์ที่พบบ่อยของผู้ติดต่อ response จริงอาจมีฟิลด์สรุปอื่นเพิ่มอีก ถ้าค้นแล้วไม่เจอ จะได้ array data ว่าง ส่วนการอ่าน record เดียวจะได้ record กลับมาตรง ๆ หรือได้ 404
ตอนสร้างผู้ติดต่อ จะไม่ใส่อีเมลกับเบอร์โทรก็ได้ ส่วนใน response ฟิลด์สองตัวนี้ชื่อ workEmail และ workPhone ดีลก็เหมือนกัน request ใช้ title แต่ response ใช้ name เวลาใน response เป็น Unix มิลลิวินาที
ID ในตัวอย่างเป็นค่าสมมติ ให้ใช้ ID ที่ได้จากเวิร์กสเปซของตัวเอง และอย่านำ record ที่ Pabbly แปลงเป็นโครงสร้างแบนไปใช้เป็น request body ตอนเขียนข้อมูล
สร้างและอัปเดต record
ส่ง JSON พร้อม header Content-Type: application/json และ Bearer key ถ้าเขียนสำเร็จจะได้ HTTP 200 พร้อม record ที่เพิ่งเขียนกลับมาตรง ๆ โดยไม่มี wrapper data ครอบ
ฟิลด์ที่ไม่บังคับจะไม่ส่งก็ได้ ตอนอัปเดต ฟิลด์ที่ไม่ได้ส่งจะคงค่าเดิมไว้ ส่วนฟิลด์ที่ระบบไม่รู้จักจะถูกปฏิเสธ record ที่โยงถึงกันและ assignedToId ต้องอยู่ในเวิร์กสเปซเดียวกัน และสมาชิกที่มอบหมายให้ต้องยัง active
POST /v1/contacts
| Field | ความหมาย |
|---|---|
firstName, lastName | บังคับ เป็นสตริงที่ไม่ว่าง |
email, phone | ไม่บังคับ เป็นสตริง |
jobTitle | ไม่บังคับ เป็นสตริง |
type | lead หรือ customer ถ้าสร้างผู้ติดต่อใหม่แล้วไม่ใส่ จะเป็น lead |
businessId | ID ของบริษัท (ไม่บังคับ) หรือ null |
assignedToId | ID ของสมาชิกที่ active (ไม่บังคับ) ถ้าไม่ใส่จะใช้คนดูแลเริ่มต้นของ key |
curl "https://elated-rat-401.convex.site/v1/contacts" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE" \
-H "Content-Type: application/json" \
--data '{"firstName":"Ada","lastName":"Lovelace"}'PATCH /v1/contacts/{id}
รับฟิลด์เดียวกับตอนสร้างผู้ติดต่อ ต้องส่งอย่างน้อย 1 ฟิลด์ ถ้าไม่ส่ง type สถานะปัจจุบันของผู้ติดต่อจะไม่เปลี่ยน ตั้ง businessId เป็น null ถ้าจะเอาบริษัทที่เชื่อมไว้ออก
{"jobTitle":"Founder"}POST /v1/contacts/{id}/tags
ส่ง tagId หรือ tagName ตัวใดตัวหนึ่งเท่านั้น (ต้องส่ง 1 ตัวพอดี) แท็กต้องมีอยู่ในเวิร์กสเปซแล้ว response คือผู้ติดต่อที่อัปเดตแล้ว ถ้าเพิ่มแท็กที่ติดอยู่แล้ว แท็กนั้นก็ยังติดอยู่แค่อันเดียว
{"tagName":"Event lead"}action find-or-create และ upsert ของ Zapier จะค้นด้วย GET /v1/contacts?email=… ก่อน แล้วค่อยสร้างหรืออัปเดตผู้ติดต่อที่ตรงกัน ขั้นตอนนี้ไม่ใช่ upsert แบบ atomic ถ้ามี workflow หลายตัวทำงานพร้อมกัน อาจชนกันได้ (race)
POST /v1/businesses
| Field | ความหมาย |
|---|---|
name | บังคับ เป็นสตริงที่ไม่ว่าง |
website, email, phone | ไม่บังคับ เป็นสตริง |
type | lead หรือ customer ถ้าไม่ใส่จะเป็น lead |
assignedToId | ID ของสมาชิกที่ active (ไม่บังคับ) |
{"name":"Example Studio","website":"https://example.com","type":"lead"}POST /v1/deals
| Field | ความหมาย |
|---|---|
title, pipelineId, stageId | บังคับ เป็นสตริง ขั้นต้องอยู่ในไปป์ไลน์ที่ระบุ |
value | ไม่บังคับ เป็นตัวเลขที่ไม่ติดลบ ค่าเริ่มต้นคือ 0 สกุลเงินจะใช้ตามเวิร์กสเปซ |
contactId, businessId | ID ของ record ที่เกี่ยวข้อง (ไม่บังคับ) |
assignedToId | ID ของสมาชิกที่ active (ไม่บังคับ) |
expectedCloseDate | วันที่ในรูป YYYY-MM-DD (ไม่บังคับ) พารามิเตอร์รุ่นเก่าตัวนี้ใช้ตั้งวันที่ต้องตามต่อ และจะส่งกลับมาในชื่อ nextFollowUp |
{"title":"Website project","pipelineId":"pipeline_123","stageId":"stage_123","value":2500,"expectedCloseDate":"2026-09-15"}response มี _id, name, value, currency, pipelineId, stageId และ nextFollowUp พร้อมรายละเอียดของ record ที่เกี่ยวข้อง ส่วน PATCH /v1/deals/{id} รับ title, value, stageId, assignedToId, expectedCloseDate และ customFields ต้องส่งอย่างน้อย 1 ฟิลด์
ตอนอัปเดตดีล ตั้ง expectedCloseDate เป็น null เพื่อล้างวันที่ต้องตามต่อ
POST /v1/tasks
| Field | ความหมาย |
|---|---|
title | บังคับ เป็นสตริงที่ไม่ว่าง |
dueDate | วันที่ในรูป YYYY-MM-DD (ไม่บังคับ) หรือ null ถ้าไม่มีวันครบกำหนด |
contactId, businessId, dealId | ID ของ record ที่เกี่ยวข้อง (ไม่บังคับ) |
assignedToId | ID ของสมาชิกที่ active (ไม่บังคับ) |
{"title":"Follow up","dueDate":"2026-09-15","contactId":"contact_123"}response มี _id, title และ dueDate พร้อมรายละเอียดของงานและ record ที่เกี่ยวข้อง วันที่เป็นวันตามปฏิทิน ไม่มี timezone ถ้ายิง API ตรง ๆ ต้องส่งเป็น YYYY-MM-DD ห้ามส่งเป็น timestamp
การสร้าง record ทั่วไปไม่รับประกัน idempotency ถ้า response หายไป ให้เช็กก่อนว่า record ถูกสร้างไปแล้วหรือยัง แล้วค่อยลองใหม่ เพราะการลองใหม่อาจสร้าง record ซ้ำอีกอัน
นำเข้าลีด
POST /v1/inbound/leads
ใช้ endpoint นี้ตอนส่งต่อลีดแบบอัตโนมัติ ให้ส่ง ID ของ event ต้นทางที่ไม่ซ้ำกันใน header Liteo-Idempotency-Key ถ้าส่ง event เดิมซ้ำ จะได้ผลลัพธ์ครั้งแรกกลับมา และไม่มีการสร้างผู้ติดต่อเพิ่ม
| Field | ความหมาย |
|---|---|
contact | บังคับ เป็น object ต้องมี firstName ส่วน lastName, email, phone และ jobTitle ไม่บังคับ |
externalIdentity | ประกอบด้วย provider, accountId และ externalId ต้องใส่ถ้าผู้ติดต่อไม่มีทั้งอีเมลและเบอร์โทร |
externalIdentity.profileUrl | URL โปรไฟล์แบบ HTTPS (ไม่บังคับ) |
business | บริษัท (ไม่บังคับ) ต้องมี name ส่วน website, email และ phone ไม่บังคับ |
tags | รายชื่อแท็กที่มีอยู่แล้ว (ไม่บังคับ) |
assignTo | อีเมลของสมาชิกเวิร์กสเปซที่ active (ไม่บังคับ) |
source | ป้ายบอกที่มา ไว้บันทึกใน activity log (ไม่บังคับ) |
curl "https://elated-rat-401.convex.site/v1/inbound/leads" \
-H "Authorization: Bearer YOUR_CONNECTION_CODE" \
-H "Liteo-Idempotency-Key: sendpilot-event-123" \
-H "Content-Type: application/json" \
--data '{"contact":{"firstName":"Ada"},"externalIdentity":{"provider":"sendpilot","accountId":"workspace_123","externalId":"lead_123","profileUrl":"https://www.linkedin.com/in/ada"},"source":"Sendpilot"}'Liteo จับคู่ด้วย externalIdentity ก่อน แล้วต่อด้วยอีเมล และเบอร์โทรเป็นอันสุดท้าย ถ้ามี event ใหม่ของ external identity เดิมเข้ามาทีหลัง Liteo จะเติมเฉพาะฟิลด์ของผู้ติดต่อที่ยังว่าง และคงค่าที่มีอยู่เดิมไว้
ถ้าใช้ Sendpilot ให้แมป eventId ไปที่ Liteo-Idempotency-Key แมป workspaceId ไปที่ accountId และแมป data.leadId ไปที่ externalId จากนั้นใช้ data.campaignId ดึงรายการลีดของแคมเปญ แล้วไล่ดูทีละหน้าจนเจอลีดที่ id ตรงกับ data.leadId และแมปชื่อ บริษัท ตำแหน่ง และ LinkedIn URL ของลีดที่เจอ
ก่อนเรียก Liteo ให้กรองด้วย customLeadStatus ของลีดที่เจอ อย่ากรองด้วย newStatus ใน webhook เพราะแค่มีคนตอบกลับธรรมดาไม่ควรสร้างผู้ติดต่อ ดูขั้นตอนครบได้ที่ ส่งลีดที่สนใจจาก Sendpilot เข้า Liteo
ถ้าใช้ Instantly ให้ subscribe lead_interested แล้วหาลีดในแคมเปญที่ตรงกันพอดีด้วย campaign_id และ lead_email จากนั้นแมป workspace ไปที่ accountId และแมป id ของลีดที่เจอไปที่ externalId โดยใช้ instantly:{workspace}:{lead_id}:lead_interested เป็น idempotency key ดูขั้นตอนครบได้ที่ ส่งลีดที่สนใจจาก Instantly เข้า Liteo
เชื่อมบริษัทกับผู้ติดต่อ
ถ้าจะบันทึกฟอร์มที่ส่งเข้ามาพร้อมทั้งบริษัทและคน ให้หาหรือสร้างบริษัทก่อน แล้วส่ง _id ของบริษัทเป็น businessId ตอนสร้างหรืออัปเดตผู้ติดต่อ
POST /v1/businesses/find-or-create
รับฟิลด์เดียวกับตอนสร้างบริษัท รวมถึง customFields ถ้าส่ง website มา จะจับคู่ด้วยโดเมนที่ normalize แล้ว ถ้าไม่มี website จะจับคู่ด้วยชื่อบริษัทที่ตัดช่องว่างหัวท้ายแล้ว และแยกตัวพิมพ์เล็กพิมพ์ใหญ่
ระบบจับคู่และสร้างให้ใน transaction เดียวกัน response จะมีบริษัทกับค่า boolean created มาด้วย บริษัทที่มีอยู่แล้วจะไม่ถูกแก้ค่า ถ้าเจอบริษัทที่ตรงกันหลายราย จะได้ 409 เพื่อให้ไปจัดการข้อมูลซ้ำเอง
{"name":"Example Studio","website":"https://example.com"}เอา _id ของบริษัทที่ได้กลับมาไปใส่ใน request ของผู้ติดต่อ
{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","businessId":"business_123"}PATCH /v1/businesses/{id}
รับ name, website, email, phone, type, assignedToId และ customFields ต้องส่งอย่างน้อย 1 ฟิลด์ ฟิลด์ที่ไม่ได้ส่งจะคงค่าเดิมไว้
ใช้ฟิลด์ที่สร้างเอง
ตอนเขียนข้อมูลผู้ติดต่อ บริษัท และดีล ส่ง object customFields ไปด้วยได้ record ใน response ก็มี customFields เช่นกัน โดยใช้ storage key ของแต่ละฟิลด์เป็น key ส่วนใน Zapier จะเห็นเป็นชื่อฟิลด์ตอนตั้งค่า action สร้างหรืออัปเดต
GET /v1/custom-fields?entityType=contact
entityType เป็น contact, business หรือ deal ก็ได้ ส่วน array data จะมี definition ที่ active สูงสุด 100 อัน แต่ละอันมี id, key, name, type และ options
{"data":[{"id":"field_123","key":"cf_budget","name":"Budget","type":"number","options":[]}]}ตอนเขียน ให้ระบุแต่ละฟิลด์ด้วย id ของ definition หรือ storage key ก็ได้ ระบบรับเฉพาะฟิลด์ที่กำหนดไว้สำหรับ record ประเภทนั้นในเวิร์กสเปซนั้น การอัปเดตจะไม่แตะฟิลด์ที่ไม่ได้แมป และ null คือล้างค่า ส่วน Zapier จะไม่แมปช่อง input ที่เว้นว่างไว้
{"customFields":{"field_123":500,"cf_source":null}}ค่าต้องตรงกับชนิดของฟิลด์ ตัวเลขต้องเป็นค่า finite (ไม่ใช่ Infinity หรือ NaN) วันที่ต้องเป็น YYYY-MM-DD ค่า dropdown ต้องตรงกับหนึ่งใน option ฟิลด์อีเมลต้องเป็นอีเมล และฟิลด์ลิงก์ต้องเป็น URL แบบ HTTP หรือ HTTPS
request เขียนข้อมูลหนึ่งครั้งใส่ custom value ได้ไม่เกิน 100 ค่า ค่าที่เป็นข้อความยาวได้ไม่เกิน 1,000 ตัวอักษร และขนาดของ request body รวมก็ยังมีเพดานอีกชั้นหนึ่งด้วย
POST /v1/custom-fields
| Field | ความหมาย |
|---|---|
entityType | contact, business หรือ deal |
name | บังคับ เป็นชื่อฟิลด์ ยาวไม่เกิน 100 ตัวอักษร |
type | text, number, date, select, link, email หรือ phone |
options | บังคับสำหรับฟิลด์ select เป็น array ของสตริงที่ไม่ซ้ำกันสูงสุด 50 ตัว ยาวไม่เกินตัวละ 50 ตัวอักษร |
เจ้าของการเชื่อมต่อต้องยังเป็นแอดมินของเวิร์กสเปซอยู่ตอนนี้ ถ้าเป็นการเชื่อมต่อแบบ service ก็คือคนที่สร้าง key ส่วนบทบาทของคนดูแลเริ่มต้น (default assignee) ไม่ได้ให้สิทธิ์นี้
ถ้าส่งชื่อ ชนิด และรายการ option ที่เหมือนกันทุกอย่าง จะได้ definition ที่มีอยู่แล้วกลับมา ถ้า definition ขัดกับของเดิมจะได้ 409 และถ้าสิทธิ์แอดมินไม่พอจะได้ 403 การสร้างฟิลด์แค่กำหนดฟิลด์ให้เวิร์กสเปซ ส่วนค่าของฟิลด์ให้ไปตั้งแยกใน action ของ record
{"entityType":"contact","name":"Budget","type":"number"}ติดตามการเปลี่ยนแปลงของ record
GET /v1/events?entityType=contact&eventType=updated
trigger อย่าง updated-record, stage-change, completion และ tag ทำงานจากประวัติการเปลี่ยนแปลงนี้ การเปลี่ยนแปลงแต่ละครั้งมี event ID ของตัวเอง แม้ record เดียวกันจะเปลี่ยนสองครั้ง ประวัติเริ่มเก็บตั้งแต่ตอน deploy ฟีเจอร์นี้ และเก็บไว้ 7 วัน
| Field | ความหมาย |
|---|---|
updated | ผู้ติดต่อ บริษัท ดีล และงาน มี changedFields กับ previousValues |
stage_changed | เฉพาะดีล มี stageId ปลายทางกับ pipelineId |
completed | เฉพาะงาน ถ้าเปิดงานใหม่แล้วทำเสร็จอีกรอบ จะได้ event ใหม่อีกอัน |
tag_added, tag_removed | ผู้ติดต่อ บริษัท และดีล มี tagId |
limit, cursor | ใช้พารามิเตอร์แบ่งหน้าแบบเดียวกับ list ของ record ถ้า payload ใหญ่ อาจได้หน้าที่สั้นลง ให้ตาม nextCursor เสมอ |
{
"data":[{
"id":"event_123","recordId":"contact_123","entityType":"contact",
"eventType":"updated","occurredAt":"2026-09-09T08:00:00.000Z",
"changedFields":["jobTitle"],
"record":{"firstName":"Ada","lastName":"Lovelace","jobTitle":"Founder"},
"previousValues":{"jobTitle":"Director"}
}],
"nextCursor":null
}ตัวอย่างนี้แสดงเฉพาะบางฟิลด์ของ record และ record ที่ถูกลบแล้วจะไม่ถูกส่งออกไป ใน Zapier เลือกให้ดูเฉพาะบางฟิลด์ได้ รวมถึงเลือกทีละฟิลด์ของฟิลด์ที่สร้างเอง หรือจะเว้นว่างไว้เพื่อรับทุกการเปลี่ยนแปลงที่รองรับก็ได้
changedFields และ previousValues บอกค่าตามที่เป็นใน event นั้น ส่วนฟิลด์อื่นของ record จะเป็นค่าปัจจุบัน ณ ตอนที่ส่งออกไป
ถ้าฟิลด์ที่สร้างเองเปลี่ยน changedFields จะมี customFields และ customFields.storage_key อยู่ด้วย Zapier จะกรองหลังจากดึงแต่ละหน้ามาแล้ว ส่วนการเปลี่ยนแปลงภายในระบบที่เป็นงานดูแลรักษาจะไม่ trigger event updated-record
Zap แต่ละตัวเช็ก event ได้สูงสุด 1,000 รายการ โดยใช้ request ไม่เกิน 10 ครั้ง ถ้า payload ใหญ่ อาจได้น้อยลง การเปลี่ยนแปลงที่อยู่นอกช่วงนี้ หรือเก่ากว่า 7 วัน อาจหลุดไป trigger เหล่านี้เป็นแบบ polling ข้อมูลจึงมาตามรอบ polling ของ Zap
ดึงรายการที่ถึงกำหนด
GET /v1/due?eventType=task_due&timezone=Asia%2FBangkok
| Field | ความหมาย |
|---|---|
task_due | งานที่ยังไม่เสร็จและครบกำหนดวันนี้ |
task_overdue | งานที่ยังไม่เสร็จและมีวันครบกำหนดก่อนวันนี้ |
deal_follow_up_due | ดีลที่ยังเปิดอยู่และมีวันที่ต้องตามต่อเป็นวันนี้ ดีลที่ชนะและดีลที่แพ้แล้วไม่รวม |
timezone | เขตเวลาแบบ IANA อย่าง Asia/Bangkok หรือ Europe/London ถ้าไม่ใส่จะเป็น UTC |
endpoint กลุ่มนี้ใช้วันที่แบบ calendar date คำว่า "วันนี้" เริ่มนับตอนเที่ยงคืนของเขตเวลาที่เลือก แม้ในช่วงที่เปลี่ยน daylight saving ก็เหมือนกัน response มี id, recordId, eventDate และ timezone มาพร้อมกับฟิลด์ของ record
id ของ event สร้างจาก record รวมกับวันครบกำหนด Zapier ใช้ ID นี้เพื่อให้ทำงานแค่ครั้งเดียวต่อ record ต่อวันที่ ไม่ใช่ทำซ้ำทุกวันที่งานยังเลยกำหนดอยู่ ถ้าย้ายวันที่ จะได้ ID ใหม่ แต่ถ้าเปิดงานใหม่โดยใช้วันที่เดิม ก็ยังเป็น ID เดิม
Task Due และ Deal Follow-Up Due จะรวมเฉพาะ record ของวันนี้ ให้เปิด Zap ทิ้งไว้ เพื่อให้ poll ตลอดวันนั้น ส่วน Task Overdue รวมวันที่ก่อนหน้านี้ด้วย Zap แต่ละตัวเช็ก record ที่ตรงเงื่อนไขได้สูงสุด 1,000 รายการต่อ poll
MCP server
POST https://app.liteo.io/api/mcp
Liteo มี MCP server ไว้ให้ AI client ทำงานในเวิร์กสเปซผ่านเครื่องมือที่มีชื่อเรียกเฉพาะ แทนการยิง HTTP ดิบ ๆ โดยใช้ Streamable HTTP ที่ endpoint เดียว ทุกเครื่องมือทำงานในฐานะสมาชิกเวิร์กสเปซตัวจริง 1 คน และใช้สิทธิ์ของสมาชิกคนนั้น
ChatGPT, claude.ai และ Claude Desktop เชื่อมด้วย OAuth ให้เพิ่ม endpoint เป็น connector แล้วกดเชื่อม ตัว client จะลงทะเบียนเอง จากนั้นก็เข้าสู่ระบบ Liteo เลือกเวิร์กสเปซ แล้วกดอนุมัติ
ขั้นตอน discovery ทำตาม MCP authorization spec โดยรองรับ dynamic client registration และ PKCE (S256) access token มีอายุ 1 ชั่วโมง ส่วน refresh token มีอายุ 30 วัน และจะออกอันใหม่ทุกครั้งที่ใช้
Claude Code, Codex และ Cursor ก็เข้าสู่ระบบด้วยวิธีนี้ได้เหมือนกัน หรือจะให้ client เหล่านี้และสคริปต์ของตัวเองส่ง API key เป็น Bearer token แทนก็ได้ สร้าง key ที่ Settings → Integrations → For developers แล้วเก็บไว้ใน environment variable และอย่าใส่ในไฟล์ที่ commit ถ้าใช้ Claude Code ไฟล์ .mcp.json ของโปรเจกต์อ่านค่าจากตัวแปรนี้ได้ดังนี้
{
"mcpServers": {
"liteo": {
"type": "http",
"url": "https://app.liteo.io/api/mcp",
"headers": { "Authorization": "Bearer ${LITEO_MCP_API_KEY}" }
}
}
}ให้เรียก get_workspace ก่อนเป็นอันดับแรก เพื่อยืนยันว่าเป็นเวิร์กสเปซไหน และการเชื่อมต่อนี้ทำงานในฐานะสมาชิกคนไหน ชื่อเครื่องมือเป็น snake_case
list รับ pageSize (ค่าเริ่มต้น 25 สูงสุด 100) กับ cursor และส่งกลับมาเป็น { items, nextCursor } วันที่เป็น YYYY-MM-DD เครื่องมือที่ใช้สร้าง record รับ idempotencyKey ได้ (ไม่บังคับ) ถ้าใส่ไว้แล้วต้องลองใหม่ ก็จะได้ผลลัพธ์แรกกลับมา แทนที่จะเกิดข้อมูลซ้ำ
| Toolset | ครอบคลุม |
|---|---|
crm | ค้นหา อ่าน สร้าง และอัปเดตผู้ติดต่อ บริษัท ดีล งาน และโน้ต รวมถึงไปป์ไลน์ สมาชิก แท็ก และฟิลด์ที่สร้างเอง |
operations | ไซต์ของลูกค้า สถานที่ สินค้าที่ติดตั้ง การเข้าเซอร์วิส และประวัติเซอร์วิส |
catalog | อ่าน สร้าง อัปเดต และเก็บถาวร (archive) สินค้า วัสดุสิ้นเปลือง และบริการ |
commerce | รายการในดีล (deal lines) ใบเสนอราคาแบบร่าง และบิลแบบร่าง |
automations | ดูรายการ automation และรอบที่รันของแต่ละตัว ส่วนการเปิดอ่านแบบเต็ม สร้าง แก้ เปิดหรือปิด และลบ ต้องเป็นแอดมินของเวิร์กสเปซ |
forms | อ่านฟอร์มและข้อมูลที่ส่งเข้ามา ส่วนการสร้าง แก้ ปิด และลบ ต้องเป็นแอดมินของเวิร์กสเปซ ฟอร์มใหม่จะเริ่มต้นแบบปิดอยู่ และต้องมีคนไปเปิดใน Liteo เอง |
เครื่องมือของแอดมินใช้ได้เฉพาะแอดมินที่เข้าสู่ระบบด้วยตัวเอง หรือใช้ personal key ส่วน workspace key ใช้ไม่ได้
มีเครื่องมือรวมทั้งหมด 76 ตัว ถ้า client จำกัดจำนวนเครื่องมือที่แสดง ให้โหลดเฉพาะบางชุดด้วย ?toolsets=crm,operations ส่วน get_workspace กับ search_crm จะมีให้เสมอ
เครื่องมือที่ลบหรือถอดข้อมูลแล้วย้อนกลับยาก ต้องใส่ confirm เป็น true ตอนเรียก ได้แก่ archive_site, remove_installed_product, delete_automation และ delete_form ใบเสนอราคากับบิลจะเป็นแบบร่างเสมอ ส่วนการส่ง ตอบรับ ออกบิลจริง ยกเลิก ลดหนี้ และรับชำระเงิน ทำได้ใน Liteo เท่านั้น server จะไม่ลบผู้ติดต่อ บริษัท หรือดีล และไม่สร้างหรือเปิด webhook action ให้เลย
ทุกการเรียกต้องผ่านการเช็กสมาชิก (membership check) billing lock และ request limit ชุดเดียวกับ REST API ถ้าไม่มีแผนที่ active จะอ่านข้อมูลได้แต่เขียนไม่ได้ แอปที่เชื่อมไว้จะแสดงอยู่ที่ Settings → Integrations → For developers ใต้ Connected apps และกดยกเลิกการเชื่อมได้ตรงนั้นเลย
ดูวิธีตั้งค่าทีละ client ได้ที่ เชื่อม AI กับ Liteo ผ่าน MCP ส่วน หน้าฟีเจอร์ MCP server จะแสดงให้เห็นว่าใช้งานจริงเป็นอย่างไร
ขีดจำกัดของ request
แต่ละ key ส่งได้ 60 request ต่อนาทีตอนทดลองใช้ และ 240 request ต่อนาทีเมื่อใช้แผนเสียเงินที่ active อยู่ นอกจากนี้ยังมีเพดานแยกอีกชั้นที่ 120 request ต่อนาทีต่อ IP ต้นทาง และเพดานนี้นับรวม request ที่ยืนยันตัวตนไม่ผ่านด้วย
key แต่ละอันใช้ได้สูงสุด 6,250 request ต่อวัน (นับตามวัน UTC) key อื่นในเวิร์กสเปซเดียวกันอาจใช้โควตารายวันนี้ร่วมกัน และ request ขอหน้าถัดไปทุกครั้งก็นับรวมในเพดานเหล่านี้ด้วย
ถ้าได้ HTTP 429 ให้รอตามจำนวนวินาทีใน Retry-After ก่อนลองใหม่ response อาจมี Liteo-RateLimit-Limit, Liteo-RateLimit-Remaining และ Liteo-RateLimit-Reset (Unix timestamp หน่วยวินาที) มาด้วย
request body แบบ JSON ต้องมีขนาดไม่เกิน 32 KB ค่าที่เป็นสตริงต้องยาวไม่เกิน 2,000 ตัวอักษร และบางฟิลด์อาจมีเพดานที่แคบกว่านั้น การจะเขียนข้อมูลได้ เวิร์กสเปซต้องอยู่บนแผนที่เปิดให้เขียนได้
รหัส error
Liteo ตอบกลับด้วย HTTP status มาตรฐานและข้อความ JSON สั้น ๆ พร้อม request ID เพื่อให้ซัพพอร์ตตามหาต้นเหตุของปัญหาได้
{
"error": {
"code": "unauthorized",
"message": "The connection code is missing, invalid, or revoked.",
"requestId": "request_123"
}
}| Status | ความหมาย |
|---|---|
400 | request ไม่ถูกต้อง |
401 | โค้ดเชื่อมต่อหายไปหรือไม่ถูกต้อง |
402 | เวิร์กสเปซต้องมีแผนที่ active ถึงจะส่ง request เขียนได้ |
403 | key นี้ไม่มีสิทธิ์เข้าถึงเวิร์กสเปซอีกแล้ว |
404 | ไม่พบ route หรือ record ในเวิร์กสเปซ |
409 | request ขัดกับสถานะปัจจุบันของ record |
429 | request มากเกินไป ให้รอแล้วลองใหม่ |
500 | ทำ request ไม่สำเร็จ ให้ติดต่อซัพพอร์ตพร้อม request ID |
503 | request นี้กำลังประมวลผลอยู่แล้ว ให้รอสักพักแล้วค่อยลองใหม่ |
ติดปัญหา?
ส่งอีเมลมาที่ hello@liteo.io ถ้า API call ล้มเหลว ให้แนบ request ID มาด้วย
อัปเดตล่าสุดเมื่อ