เอกสาร Messaging API
ดึงแชทในแอป PropertyHub เข้า inbox ของเอเจนต์ ใช้ได้แค่ URL, header และฟิลด์ JSON ในหน้านี้
ภาพรวม
ใช้ API นี้ดึงแชทในแอป PropertyHub เข้า inbox ของคุณ (Pancake, CRM หรือระบบที่รับ HTTPS webhook ได้)
ใช้ได้แค่ REST กับ webhook ในหน้านี้ ไม่ต้องเข้าหลังบ้าน PropertyHub รูปแบบใกล้เคียง LINE เพื่อให้อินบ็อกซ์ใช้โค้ดเดิมได้
API นี้คือ transport (auth, webhook, reply/push, ประวัติ) ไม่ได้กำหนดว่า inbox ใน CRM จะหน้าตาอย่างไร แต่ละระบบออกแบบเอง
เชื่อมตามลำดับนี้:
- เก็บ
channelId,channelSecretและchannelAccessTokenจากเอเจนต์ (secret กับ token แสดงครั้งเดียว) - ลงทะเบียน webhook HTTPS ด้วย
PUT /channel/webhook/endpointแล้วเรียกPOST /channel/webhook/test - ทุกครั้งที่ POST มาที่ URL ของคุณ: verify signature จาก raw body ตอบ 2xx ให้เร็ว เก็บ event ไว้ แล้วค่อยประมวลผลทีหลัง
- room key คือ
userIdของอีกฝ่าย (บน webhook แบบ message คือsource.userId) อย่าใช้conversationIdเป็น room key ค่านั้นใช้แค่GET /conversations/:id/messages - ส่งได้สองแบบ ดู CRM ใช้ push ด้วย userId จาก
GET /conversationsได้เลย reply เป็นทางเลือก (รูปแบบ LINE มี token 24 ชม. จาก webhook) ทั้งสองทาง billing ไม่ต่างกันบน PropertyHub
ด้านล่างนี้คือ field-level spec
Production
https://api.propertyhub.in.th/api/v1/messagingAuth ของทุก REST call: Authorization: Bearer {channelAccessToken}
ความหมายของ id
ค่าที่คุณจะเจอมีแค่นี้ มองเป็นข้อความธรรมดา ไม่ต้องรู้ว่าเก็บในฐานข้อมูลแบบไหน
| ฟิลด์ที่เห็น | ความหมาย |
|---|---|
channelId / destinationch_9f2c... | รหัสช่อง Messaging ที่เอเจนต์เปิดไว้ ใช้บอกว่าเป็นช่องของเอเจนต์คนไหน ไม่ใช่รหัสผู้ซื้อหรือเอเจนต์ |
userIdph_user_42 | คนใน PropertyHub บน REST ค่า to และ /profile/:userId รับเลข 42 ได้ด้วย webhook ส่งเป็น ph_user_42 เสมอ |
conversationId / id1042 | ใช้ดึงประวัติข้อความเท่านั้น ไม่ใช่ room key |
message id88213 | ข้อความหนึ่งข้อความ |
room key = userId ของอีกฝ่าย
บน webhook คือ source.userId บนรายการบทสนทนาคือ userId ตอน push คือ to
- ตอน
userIdใน webhook_verify คือเอเจนต์ที่เพิ่งเชื่อมต่อ - ตอน event message ค่า
source.userIdคืออีกฝ่าย (คนที่ส่งมา) destinationคือรหัสช่องเดียวกับchannelIdใช้บอกว่า webhook นี้เป็นของช่องไหน ไม่ใช่รหัสคนที่คุยด้วย อย่าเอาไปเป็น room key หรือค่าto
รับ credential
เอเจนต์เปิด Messaging API ในหน้าตั้งค่า PropertyHub แล้วคัดลอกสามค่านี้:
| ค่า | ทำอะไรกับมัน |
|---|---|
channelId | เก็บไว้ จะมาอีกครั้งเป็น destination ใน webhook |
channelSecret | verify signature ของ webhook |
channelAccessToken | Bearer token ของทุก REST call |
ถ้าเปลี่ยน secret หรือ token ใหม่ ค่าเดิมใช้ไม่ได้ทันที
ข้อความจากผู้ซื้อจะถูกส่งต่อให้คุณ หลังจากลงทะเบียน webhook URL แล้วเท่านั้น
ลงทะเบียน webhook
เอเจนต์ไม่ควรไปวาง webhook URL ใน PropertyHub หลังจากเก็บ token แล้ว ให้ระบบของคุณลงทะเบียนเอง
Webhook บน production ต้องเป็น https สาธารณะ ชื่อฟิลด์ตาม LINE คือ endpoint (รับ webhookUrl / active / webhookEnabled ได้เช่นกัน) ถ้าระบุ URL จะเปิด webhook เว้นแต่ส่ง active: false
PUT /channel/webhook/endpoint
Content-Type: application/json
{ "endpoint": "https://your-inbox.example/webhooks/propertyhub" }คำตอบ 200 ของ PUT และ GET เป็น Object เดียวกัน:
{
"endpoint": "https://your-inbox.example/webhooks/propertyhub",
"active": true,
"userId": "ph_user_42",
"displayName": "สมชาย",
"pictureUrl": "https://cdn.example/avatar.jpg"
}userId ในนี้คือเอเจนต์ที่เชื่อมต่อ ผูกบัญชี inbox ของคุณกับ ph_user_42
ไม่มี DELETE ปิด webhook ด้วย { "active": false } หรือ { "endpoint": "" }
API จริงต้องใช้ https สาธารณะ โฮสต์ส่วนตัวหรือ loopback ใช้ไม่ได้ http://localhost ใช้ได้เฉพาะตอนยิง API บนเครื่องตัวเอง ถ้าจะลงทะเบียนจากโน้ตบุ๊กไป API จริง ให้ใช้ https tunnel เช่น ngrok หรือ Cloudflare
GET /channel/webhook/endpoint คืน Object เดียวกัน
POST /channel/webhook/test จะให้ PropertyHub POST event webhook_verify ไปที่ URL ของคุณ (มี signature เหมือน webhook จริง) URL ควรตอบ 2xx จากนั้น REST ได้คำตอบแบบนี้
{
"success": true,
"timestamp": "2026-09-14T04:00:00.000Z",
"statusCode": 200,
"reason": "OK",
"detail": "ส่ง webhook_verify ไปที่ https://your-inbox.example/webhooks/propertyhub แล้ว ปลายทางตอบ HTTP 200",
"endpoint": "https://your-inbox.example/webhooks/propertyhub",
"active": true,
"userId": "ph_user_42",
"displayName": "สมชาย",
"pictureUrl": "https://cdn.example/avatar.jpg"
}Body ที่ PropertyHub ส่งมาที่คุณ:
{
"destination": "ch_9f2c...",
"userId": "ph_user_42",
"displayName": "สมชาย",
"pictureUrl": "https://cdn.example/avatar.jpg",
"events": [
{
"mode": "active",
"type": "webhook_verify",
"timestamp": 1788156956732,
"webhookEventId": "01M1B7BMC63WBEJYT90GJXVRDZ",
"deliveryContext": { "isRedelivery": false },
"userId": "ph_user_42",
"displayName": "สมชาย",
"pictureUrl": "https://cdn.example/avatar.jpg"
}
]
}บทสนทนา
หนึ่งแถวคือแชท 1:1 ของเอเจนต์คนนี้กับอีกคนหนึ่ง ไม่ใช่รายการผู้ใช้ทั้งระบบ
Pagination
ใช้กับ GET /conversations และ GET /conversations/:id/messages
| Query | ค่าเริ่มต้น | หมายเหตุ |
|---|---|---|
page | 1 | เริ่มที่หน้า 1 |
perPage | บทสนทนา 30, ข้อความ 10 | สูงสุด 100 |
order | asc | เฉพาะประวัติข้อความ asc หรือ desc ตามเวลา |
{
"pagination": {
"page": 1,
"perPage": 30,
"totalCount": 128
}
}totalCount คือจำนวนแถวทั้งหมด ไม่ใช่ความยาวของหน้าปัจจุบัน
รายการบทสนทนา
GET /conversations?page=1&perPage=30
userId / name / pictureUrl คืออีกฝ่าย ไม่ใช่เอเจนต์เจ้าของ channel
{
"conversations": [
{
"id": "1042",
"userId": "ph_user_20481",
"name": "มะลิ",
"pictureUrl": "https://cdn.example/mali.jpg",
"lastMessageTimestamp": "2026-09-14T03:55:12.000Z",
"unreadCount": 2
}
],
"pagination": {
"page": 1,
"perPage": 30,
"totalCount": 1
}
}| ฟิลด์ | ความหมาย |
|---|---|
id | conversationId สำหรับดึงประวัติ |
userId | อีกฝ่าย ใช้เป็น room key และเป็น to ตอน push |
name | ชื่อที่แสดง |
pictureUrl | รูปโปรไฟล์ หรือ null |
lastMessageTimestamp | เวลาข้อความล่าสุด |
unreadCount | ข้อความที่เอเจนต์ยังไม่อ่าน |
ประวัติข้อความ
GET /conversations/1042/messages?page=1&perPage=10&order=asc
ใช้ id จากรายการบทสนทนา หรือ conversationId
{
"conversationId": "1042",
"messages": [
{
"id": "88210",
"type": "text",
"text": "สวัสดีค่ะ ห้องนี้ยังว่างไหมคะ",
"senderId": "ph_user_20481",
"createdAt": "2026-09-14T03:50:00.000Z"
},
{
"id": "88211",
"type": "listing",
"text": "https://propertyhub.in.th/listings/99",
"listingId": "99",
"url": "https://propertyhub.in.th/listings/99",
"senderId": "ph_user_42",
"createdAt": "2026-09-14T03:51:00.000Z"
}
],
"pagination": {
"page": 1,
"perPage": 10,
"totalCount": 2
}
}| ฟิลด์ | ความหมาย |
|---|---|
senderId | ใครส่ง (ph_user_...) |
createdAt | เวลา ISO |
type | รูปแบบเดียวกับประเภทข้อความด้านล่าง |
โปรไฟล์
GET /profile/ph_user_20481
ใช้ได้กับเอเจนต์ หรือคนที่เอเจนต์มีแชท 1:1 อยู่แล้ว นอกนั้นได้ 404 USER_NOT_FOUND
:userId รับ ph_user_20481 หรือ 20481
{
"userId": "ph_user_20481",
"displayName": "มะลิ",
"pictureUrl": "https://cdn.example/mali.jpg",
"name": "มะลิ"
}name คือค่าเดียวกับ displayName
ประเภทข้อความ
ค่า type บน webhook และประวัติเป็นตัวพิมพ์เล็ก: text, image, video, listing, link, phone
คำตอบ reply/push ช่อง messages[].type เป็นค่าที่เก็บแบบตัวพิมพ์ใหญ่ เช่น TEXT, LISTING
ข้อความประกาศใน webhook และประวัติใช้ type: "listing" ไม่ใช่ type: "text" แล้วแนบข้อมูลประกาศเพิ่ม
timestamp ใน webhook เป็นมิลลิวินาที createdAt / lastMessageTimestamp เป็น ISO-8601 conversationId เป็นสตริงเสมอ เช่น "1042"
ยังไม่รองรับ: sticker, location, file, audio, กลุ่ม, follow / unfollow / postback
ส่ง (inbox ของคุณ → PropertyHub)
ส่งได้สูงสุด 5 Object/request
{ "type": "text", "text": "ห้องนี้ยังว่างครับ" }{
"type": "image",
"originalContentUrl": "https://example.com/room.jpg",
"previewImageUrl": "https://example.com/room-thumb.jpg"
}video ใช้รูปแบบเดียวกับ image ต้องมี originalContentUrl (หรือ contentProvider.originalContentUrl) เฟส 1 แอป PropertyHub แสดงรูป/วิดีโอจากพาร์ทเนอร์เป็น URL ไม่ใช่รูปในแกลเลอรี
{ "type": "listing", "listingId": "99" }{ "type": "link", "url": "https://example.com" }ลิงก์ส่ง title หรือ text เป็น label ได้ phone ส่ง text แทน phoneNumber ได้ text เกินบน listing ไม่ถูกเก็บ ใช้แค่ listingId
{ "type": "phone", "phoneNumber": "0891234567" }รับ (PropertyHub → webhook ของคุณ)
render listing.url (ค่าเดียวกับ text ) เป็นลิงก์ที่คลิกได้ อย่าไปสร้างการ์ดประกาศจาก listingId
{
"id": "7",
"type": "listing",
"text": "https://propertyhub.in.th/listings/99",
"listingId": "99",
"url": "https://propertyhub.in.th/listings/99"
}{
"id": "8",
"type": "link",
"text": "https://example.com",
"url": "https://example.com"
}{
"id": "9",
"type": "phone",
"text": "0891234567",
"phoneNumber": "0891234567"
}รูปและวิดีโอจากแอป PropertyHub contentProvider คือไฟล์แรก ถ้า attachments มีมากกว่าหนึ่งรายการให้ render ทุกไฟล์ attachments[].id เป็นตัวเลข:
{
"id": "88213",
"type": "image",
"contentProvider": {
"type": "external",
"originalContentUrl": "https://cdn.example/original.jpg",
"previewImageUrl": "https://cdn.example/thumb.jpg"
},
"attachments": [
{
"id": 991,
"thumbnailUrl": "https://cdn.example/thumb.jpg",
"pictureUrl": "https://cdn.example/large.jpg",
"originalUrl": "https://cdn.example/original.jpg"
}
]
}{
"id": "88214",
"type": "video",
"contentProvider": {
"type": "external",
"originalContentUrl": "https://cdn.example/clip.mp4",
"previewImageUrl": "https://cdn.example/clip-thumb.jpg"
},
"attachments": [
{
"id": 992,
"thumbnailUrl": "https://cdn.example/clip-thumb.jpg",
"pictureUrl": "https://cdn.example/clip-thumb.jpg",
"originalUrl": "https://cdn.example/clip.mp4"
}
]
}ไม่มี URL ดาวน์โหลดตาม id ให้ใช้ originalContentUrl หรือ attachments[].originalUrl
สองวิธีในการส่ง
ทั้งสองทางส่งข้อความเข้าแชท 1:1 เดียวกัน PropertyHub ไม่คิด billing ต่างกัน (บน LINE ค่า reply ฟรีในช่วงเวลาหนึ่ง แต่ push เสียเงิน) เลือกแบบที่เข้ากับ inbox ของคุณ
ต่างจาก LINE การเชื่อม API นี้ดึงแชทที่มีอยู่แล้วได้จาก GET /conversations ไม่ต้องรอ webhook ใหม่ก่อนถึงจะส่งได้
| POST /message/reply | POST /message/push | |
|---|---|---|
| ส่งหาใคร | ตาม replyToken | to เป็น ph_user_... ของอีกฝ่ายในห้อง |
| มีเมื่อไร | มีเฉพาะหลัง webhook แบบ message (ลูกค้าทักเอเจนต์) | เมื่อมีห้อง 1:1 อยู่แล้ว |
| ข้อจำกัด | 24 ชั่วโมง ครั้งเดียว | ตราบที่บทสนทนายังมีอยู่ |
| ไม่ใช่ | การ quote ข้อความ ไม่มี quoteToken | การเริ่มแชทกับผู้ซื้อคนใหม่ที่ยังไม่เคยทัก |
แบบ CRM (แนะนำถ้ามีรายการห้องแล้ว): เปิดบทสนทนา แล้ว push ไปที่ userId
แบบ LINE clone: เพิ่งได้ webhook และยังมี replyToken ให้เรียก reply ไม่เช่นนั้นใช้ push
webhook_verify ไม่มี replyToken
Reply
นี่คือการตอบ event ที่เข้ามาแบบ LINE ไม่ใช่การ quote ข้อความก่อนหน้า ข้อความไปลงแชท 1:1 เดียวกับ webhook ไม่มี quoteToken
เมื่อ webhook แบบ message มี replyToken
POST /message/reply
{
"replyToken": "5206668982094dcd98c5b784bafca70c",
"messages": [{ "type": "text", "text": "ได้ครับ ว่างอยู่" }]
}replyToken ใช้ได้ 24 ชั่วโมง และใช้ได้ครั้งเดียว
{
"message": "Invalid replyToken",
"error": { "code": "INVALID_REPLY_TOKEN", "message": "Invalid replyToken" }
}optional header X-PropertyHub-Retry-Key
{
"sentMessages": [{ "id": "88213" }],
"messages": [
{
"id": "88213",
"conversationId": "1042",
"type": "TEXT",
"createdAt": "2026-09-14T03:56:00.000Z"
}
]
}sentMessages คือรายการ id แบบ LINE messages[].type คือค่าที่เก็บ (TEXT, LISTING, LINK, PHONE, …) conversationId ใช้ดึงประวัติเท่านั้น
ข้อความที่คุณส่งด้วย reply จะไม่ถูก POST กลับมาที่ webhook ของคุณ
Push
ใช้ส่งเข้าห้อง 1:1 ที่มีอยู่แล้ว ค่า to คือ userId ของบทสนทนาจาก GET /conversations หรือ source.userId จาก webhook ไม่ต้องมี replyToken
POST /message/push
{
"to": "ph_user_20481",
"messages": [{ "type": "text", "text": "สวัสดีครับ" }]
}ค่า to รับ ph_user_20481 หรือ 20481
ผู้ซื้อคนใหม่ต้องทักจากแอป PropertyHub ก่อน หลังจาก inbound ครั้งแรก GET /conversations จะมีห้อง และ push ได้แม้ไม่เคยใช้ reply to ต้องเป็นคนที่เอเจนต์คุยด้วยแล้ว คำตอบ 200 รูปเดียวกับ reply
{
"message": "Can only push to a user you already have a conversation with",
"error": {
"code": "CONVERSATION_NOT_FOUND",
"message": "Can only push to a user you already have a conversation with"
}
}404 CONVERSATION_NOT_FOUND คือยังไม่มีแชท 1:1 400 INVALID_USER_ID
ค่า X-PropertyHub-Retry-Key เป็นสตริงไม่ว่าง ไม่มีวันหมดอายุ ถ้าได้ 409 ให้รอประมาณ 1 วินาทีแล้วส่งคีย์เดิมซ้ำ
Webhooks
PropertyHub POST JSON ไปที่ endpoint ของคุณ พร้อม Content-Type: application/json และ User-Agent: PropertyHub-Messaging-Webhook/1.0
คุณต้องตอบอะไร
| สถานะที่คุณตอบ | PropertyHub ทำอะไร |
|---|---|
| 2xx (ไม่สนใจ body ตอบ 200 ว่างได้) | สำเร็จ ไม่ส่งซ้ำ |
| หมดเวลา 10 วินาที, เน็ตหลุด, 3xx (ไม่ตาม redirect), 4xx รวม 401, 5xx หรือ body เป็น HTML | นับว่าล้ม แล้วส่งซ้ำ |
verify signature แล้ว enqueue จากนั้นตอบ 2xx อย่ารองานข้างหลังก่อนตอบ
ถ้าคุณตอบ 401 เราถือว่าล้มแล้วส่งซ้ำ ตอบ 2xx เมื่อรับ body แล้ว
Retry
ใช้ webhookEventId และ replyToken เดิม และ deliveryContext.isRedelivery เป็น true
สูงสุด 3 ครั้ง รอ 1 วินาทีแล้ว 2 วินาที ถ้ายังไม่สำเร็จอาจมี background worker ส่งอีก dedupe ที่ webhookEventId แล้วตอบ 2xx แม้ซ้ำ
แชทในแอปยังสำเร็จ แม้ webhook ของคุณล่ม
Signature
Header (ค่าเดียวกัน): X-Line-Signature และ X-PropertyHub-Signature
ค่า channelSecret เป็นสตริง hex 64 ตัว ใช้เป็นข้อความ UTF-8 อย่าแปลง hex เป็นไบต์
const crypto = require('crypto');
const expected = crypto
.createHmac('sha256', channelSecret)
.update(rawBody)
.digest('base64');rawBody ต้องเป็นไบต์ดิบของคำขอ ไม่ใช่objectที่ stringify ใหม่
เทียบ destination กับ channelId ได้ ถ้า URL เดียวรับหลายเอเจนต์
Events
ตอนนี้ events[] มีหนึ่งรายการต่อ POST แต่ให้วนลูปและ dedupe ต่อ webhookEventId
mode เป็น active เสมอ ไม่มี standby ละไว้ได้
source.type เป็น user เสมอ ไม่มี source.type: "agent"
เมื่ออีกฝ่ายพิมพ์ในแอป PropertyHub คุณได้ event message และ replyToken
เมื่อเอเจนต์พิมพ์ในแอป PropertyHub webhook ของคุณจะไม่ถูกเรียก ให้บันทึกข้อความตอนเรียก reply/push เอง
event message
{
"destination": "ch_9f2c...",
"events": [
{
"mode": "active",
"type": "message",
"source": { "type": "user", "userId": "ph_user_20481" },
"message": {
"id": "88210",
"type": "text",
"text": "สวัสดีค่ะ ห้องนี้ยังว่างไหมคะ"
},
"timestamp": 1788156956732,
"replyToken": "5206668982094dcd98c5b784bafca70c",
"webhookEventId": "01M1B7BMC63WBEJYT90GJXVRDZ",
"deliveryContext": { "isRedelivery": false },
"conversationId": "1042"
}
]
}ประกาศใช้ type listing ไม่ใช่ text
{
"destination": "ch_9f2c...",
"events": [
{
"mode": "active",
"type": "message",
"source": { "type": "user", "userId": "ph_user_20481" },
"message": {
"id": "7",
"type": "listing",
"text": "https://propertyhub.in.th/listings/99",
"listingId": "99",
"url": "https://propertyhub.in.th/listings/99"
},
"timestamp": 1788157000000,
"replyToken": "5206668982094dcd98c5b784bafca70c",
"webhookEventId": "01M1B7BMC63WBEJYT90GJXVRDZ",
"deliveryContext": { "isRedelivery": false },
"conversationId": "1042"
}
]
}| ฟิลด์ | ความหมาย |
|---|---|
destination | รหัสช่องเดียวกับ channelId บอกว่าเป็นช่องของเอเจนต์คนไหน ไม่ใช่รหัสคนที่คุยด้วย |
source.userId | อีกฝ่าย room key |
message | ดูประเภทข้อความ |
replyToken | ใช้กับ /message/reply |
conversationId | ใช้ดึงประวัติเท่านั้น (สตริง) |
webhookEventId | dedup key |
Error Response
อ่านข้อความจากช่องใดช่องหนึ่งได้
{
"message": "Invalid userId",
"error": { "code": "INVALID_USER_ID", "message": "Invalid userId" }
}อ่าน message หรือ error.message ค่า error.code
คำตอบ 401 จาก auth อาจไม่มีช่อง message ด้านบน
{
"error": { "code": "UNAUTHORIZED", "message": "Invalid channel access token" }
}| HTTP | error.code | เมื่อไร |
|---|---|---|
| 400 | INVALID_USER_ID | to หรือ :userId ไม่ถูกต้อง หรือ push หาเอเจนต์เอง |
| 400 | INVALID_REPLY_TOKEN | ไม่มี token หรือใช้ไปแล้ว / หมดอายุ |
| 400 | INVALID_MESSAGES | ไม่มี messages หรือว่าง |
| 400 | TOO_MANY_MESSAGES | มากกว่า 5 ข้อความในคำขอเดียว |
| 400 | INVALID_MESSAGE | image/video ไม่มี originalContentUrl |
| 400 | UNSUPPORTED_MESSAGE_TYPE | type ที่ไม่รู้จัก |
| 400 | INVALID_REQUEST | PUT webhook ไม่มี endpoint |
| 400 | MESSAGING_INVALID_WEBHOOK_URL | URL webhook ไม่ใช่ https สาธารณะ |
| 400 | MESSAGING_CHANNEL_DISABLED | ทดสอบ webhook ทั้งที่ channel ถูกปิด |
| 401 | UNAUTHORIZED | ไม่มี token หรือผิด หรือถูกปิด |
| 404 | CONVERSATION_NOT_FOUND | push หรือประวัติของแชทที่เอเจนต์คนนี้ไม่มี |
| 404 | USER_NOT_FOUND | โปรไฟล์ของคนที่ยังไม่เคยคุยด้วย |
| 409 | REQUEST_IN_PROGRESS | X-PropertyHub-Retry-Key ชุดเดียวกันกำลังทำงานอยู่ |
| 500 | INTERNAL_ERROR | ข้อผิดพลาดฝั่งเซิร์ฟเวอร์ |
REST ไม่มีรหัส INVALID_SIGNATURE signature มีเฉพาะบน webhook ที่คุณรับ