Đăng ngày

Quản lý hội thoại Pancake API — Tag, Assign nhân viên, Mark Read/Unread

Đăng ngày
  • avatar
    Name
    Định Phan - netFull
    Twitter

Quản lý hội thoại Pancake API — Tag, Assign nhân viên, Mark Read/Unread

TL;DR: Pancake cung cấp 4 nhóm endpoint quản lý workflow CRM trong conversation: POST /conversations/{id}/tags (add/remove tag), POST /conversations/{id}/assign (assign nhân viên xử lý), POST /conversations/{id}/read/unread (đánh dấu đã/chưa đọc). Kèm 2 endpoint lookup: GET /pages/{id}/tagsGET /pages/{id}/users. Tất cả ở public_api/v1, dùng page_access_token. Bonus: Pancake có tính năng round-robin tự động built-in — bạn config 1 lần ở dashboard, hệ thống sẽ tự assign hội thoại mới cho nhân viên theo lượt, không cần tự code.

Sau khi đã biết cách lấy danh sách hội thoạiđồng bộ khách hàng từ Pancake, bước tiếp theo trong CRM workflow là quản lý vòng đời của mỗi hội thoại: phân loại bằng tag, giao việc cho nhân viên, đánh dấu cần follow-up sau. Bài này cover đầy đủ 4 endpoint workflow + 2 endpoint lookup, kèm pattern thực chiến.

Nếu chưa quen với Pancake API, đọc trước bài giới thiệubài webhook + reply API.


1. Khi nào cần dùng các endpoint này?

Use caseEndpoint chính
Auto-tag conversation theo keyword tin nhắn (vd: "mua/đặt/order" → tag "Đặt hàng")POST /tags
Phân ca trực cho nhân viên sales tự động theo round-robinPOST /assign
Mark unread khách cần follow-up sau (vd: hứa gọi lại sáng mai)POST /unread
Bulk re-assign khi nhân viên nghỉ phépPOST /assign
Sync với CRM nội bộ — tag Pancake = pipeline stage Salesforce/HubSpotPOST /tags
Báo cáo KPI nhân viên theo conversation đã xử lýPOST /assign + tracking

TIP

Trước khi tự code logic phức tạp, kiểm tra dashboard Pancake → Cài đặt → Phân chia hội thoại tự động xem feature có sẵn nào dùng được. Một số case (round-robin, auto-tag theo từ khoá đơn giản) có UI config no-code.


2. Chuẩn bị Page Access Token

Toàn bộ endpoint trong bài này cần page_access_token. Cách lấy đã có trong bài Webhook & API gửi tin nhắn.


3. Lấy danh sách Tag của page

Trước khi gắn tag cho conversation, bạn cần biết tag_id — không thể dùng tag name trực tiếp.

GET https://pages.fm/api/public_api/v1/pages/{page_id}/tags?page_access_token={token}

cURL example

curl -G "https://pages.fm/api/public_api/v1/pages/$PAGE_ID/tags" \
  --data-urlencode "page_access_token=$TOKEN"

Response shape

{
  "tags": [
    { "id": 0, "text": "Kiểm hàng", "color": "#4b5577", "lighten_color": "#c9ccd6" },
    { "id": 1, "text": "Đặt hàng", "color": "#2ecc71", "lighten_color": "#a3e4be" },
    { "id": 2, "text": "Khiếu nại", "color": "#e74c3c", "lighten_color": "#f5b8b0" }
  ]
}

WARNING

id ở response là integer (0, 1, 2...), nhưng khi POST add/remove tag thì truyền dạng string. Pancake chấp nhận cả 2 form ở request body nhưng cứ string cho an toàn.

Pattern: build map name → id để dùng

const BASE = 'https://pages.fm/api/public_api/v1'

async function loadTagMap(pageId, token) {
  const r = await fetch(`${BASE}/pages/${pageId}/tags?page_access_token=${token}`)
  const { tags = [] } = await r.json()
  return Object.fromEntries(tags.map((t) => [t.text.toLowerCase(), String(t.id)]))
}

// Sử dụng:
const tagMap = await loadTagMap(PAGE_ID, TOKEN)
const orderTagId = tagMap['đặt hàng']    // '1'

Cache tagMap trong memory (TTL 1 giờ) — tag ít thay đổi, không cần fetch mỗi lần.


4. Add / Remove tag cho conversation

POST https://pages.fm/api/public_api/v1/pages/{page_id}/conversations/{conversation_id}/tags?page_access_token={token}

Request body

{
  "action": "add",          // hoặc "remove"
  "tag_id": "1"
}

cURL — add tag "Đặt hàng"

curl -X POST "https://pages.fm/api/public_api/v1/pages/$PAGE_ID/conversations/$CONV_ID/tags?page_access_token=$TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "action": "add", "tag_id": "1" }'

Response

{
  "data": [0, 1],              // mảng tag_id của conversation SAU khi update
  "success": true,
  "timestamp": 1717900800
}

IMPORTANT

Mỗi request chỉ thao tác 1 tag. Muốn add nhiều tag cho 1 conversation → loop, throttle dưới 5 req/s (xem rate limit).

Pattern: auto-tag từ webhook theo keyword

// Webhook handler nhận message event
async function onMessageReceived(event, tagMap) {
  const text = event.message?.text?.toLowerCase() || ''

  const keywordToTag = {
    'đặt hàng': 'đặt hàng',
    'mua': 'đặt hàng',
    'order': 'đặt hàng',
    'giá': 'hỏi giá',
    'khiếu nại': 'khiếu nại',
    'complaint': 'khiếu nại',
  }

  const tagsToAdd = new Set()
  for (const [kw, tagName] of Object.entries(keywordToTag)) {
    if (text.includes(kw) && tagMap[tagName]) {
      tagsToAdd.add(tagMap[tagName])
    }
  }

  for (const tagId of tagsToAdd) {
    await fetch(
      `${BASE}/pages/${event.page_id}/conversations/${event.conversation_id}/tags?page_access_token=${TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'add', tag_id: tagId }),
      }
    )
    await new Promise((r) => setTimeout(r, 250))  // throttle
  }
}

5. Lấy danh sách nhân viên của page

Cần user.id để dùng cho assign — không dùng được tên hay Facebook ID.

GET https://pages.fm/api/public_api/v1/pages/{page_id}/users?page_access_token={token}

Response shape

{
  "success": true,
  "users": [
    {
      "id": "c4bafd84-7b96-4f28-b59a-031f17c32ddf",   // UUID — dùng cái này cho assign
      "name": "Nguyễn Văn A",
      "fb_id": "116256249766099",
      "status": "available",                            // available | busy | offline
      "is_online": true,
      "status_in_page": "active",
      "page_permissions": { "permissions": [100, 71, 81] }
    }
  ],
  "disabled_users": [
    { "id": "...", "name": "...", "fb_id": "..." }
  ],
  "round_robin_users": {
    "inbox": ["fb5ff8ed-434b-4d4b-a213-b595b242b81a"],
    "comment": ["79d4e769-ac31-4821-8304-d6e251d532e9"]
  }
}

3 trường quan trọng:

  • users: nhân viên đang active — dùng để assign
  • disabled_users: nhân viên đã bị disable (nghỉ việc) — không assign nữa, nhưng vẫn cần để map history
  • round_robin_users: danh sách user đang trong vòng round-robin do Pancake quản lý sẵn

TIP

Pancake có round-robin built-in — bạn config danh sách user trong dashboard hoặc qua POST /pages/{page_id}/round_robin_users, Pancake sẽ tự động assign hội thoại mới cho nhân viên tiếp theo theo lượt. Nếu round-robin đơn giản đã đủ → không cần tự code logic ở section 6.4. Chỉ tự code khi cần custom (vd: assign theo skill, theo ngôn ngữ, theo VIP tier). Tham khảo tài liệu chính thức Pancake về Phân chia hội thoại tự động.


6. Assign nhân viên cho conversation

POST https://pages.fm/api/public_api/v1/pages/{page_id}/conversations/{conversation_id}/assign?page_access_token={token}

Request body

{
  "assignee_ids": [
    "c4bafd84-7b96-4f28-b59a-031f17c32ddf",
    "fb5ff8ed-434b-4d4b-a213-b595b242b81a"
  ]
}

cURL — assign cho 1 nhân viên

curl -X POST "https://pages.fm/api/public_api/v1/pages/$PAGE_ID/conversations/$CONV_ID/assign?page_access_token=$TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "assignee_ids": ["c4bafd84-7b96-4f28-b59a-031f17c32ddf"] }'

WARNING

assignee_idsmảng thay thế hoàn toàn, không append. Truyền [] → bỏ assign toàn bộ. Truyền 1 user mới mà không kèm user cũ → user cũ bị bỏ assign. Muốn thêm assignee, phải GET conversation, merge mảng, rồi POST lại.

Pattern: round-robin tự build (khi cần custom)

Lưu cursor trong DB hoặc Redis. Mỗi lần có conversation cần assign → lấy user tiếp theo theo index.

// Giả sử có sẵn danh sách availableUsers từ /users API, lọc status='available'
async function roundRobinAssign(pageId, conversationId, availableUsers, db) {
  if (availableUsers.length === 0) return  // không ai available

  // Lấy index cuối từ DB
  const row = await db.query(
    `SELECT idx FROM rr_cursor WHERE page_id = $1`, [pageId]
  )
  const lastIdx = row.rows[0]?.idx ?? -1
  const nextIdx = (lastIdx + 1) % availableUsers.length
  const targetUser = availableUsers[nextIdx]

  await fetch(
    `${BASE}/pages/${pageId}/conversations/${conversationId}/assign?page_access_token=${TOKEN}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ assignee_ids: [targetUser.id] }),
    }
  )

  // Save cursor
  await db.query(
    `INSERT INTO rr_cursor (page_id, idx) VALUES ($1, $2)
     ON CONFLICT (page_id) DO UPDATE SET idx = $2`,
    [pageId, nextIdx]
  )
}

TIP

Nhắc lại: nếu không có yêu cầu custom đặc biệt (assign theo skill / ngôn ngữ / VIP tier), dùng Pancake round-robin built-in thay vì tự code — tiết kiệm cả 1 cron job và DB table.


7. Mark Read / Unread

Hai endpoint đơn giản, không có body:

# Mark đã đọc
curl -X POST "https://pages.fm/api/public_api/v1/pages/$PAGE_ID/conversations/$CONV_ID/read?page_access_token=$TOKEN"

# Mark chưa đọc — dùng cho follow-up sau
curl -X POST "https://pages.fm/api/public_api/v1/pages/$PAGE_ID/conversations/$CONV_ID/unread?page_access_token=$TOKEN"

Use case Mark Unread

  • Hẹn follow-up: khách hỏi "gọi lại sáng mai" → mark unread → sáng mai nhân viên mở Pancake thấy bubble đỏ → biết phải reply
  • Hand-off ca trực: ca tối chưa xử lý xong → mark unread để ca sáng tiếp tục
  • Bulk re-open: cron job mark unread cho conversation đã read >24h mà chưa được reply

Use case Mark Read

  • Sau auto-reply bot: bot đã reply → mark read để không hiện trong inbox của nhân viên
  • Spam filter: tin spam đã detect → mark read + tag "Spam" → không quấy rầy nhân viên

8. Workflow tổng hợp — webhook → auto-tag → auto-assign

Combine 3 endpoint trên thành 1 pipeline production-grade:

import { Client } from 'pg'

const BASE = 'https://pages.fm/api/public_api/v1'
const PAGE_ID = process.env.PANCAKE_PAGE_ID
const TOKEN = process.env.PANCAKE_PAGE_ACCESS_TOKEN

// Cache tag map + users map, refresh mỗi 1 giờ
let cache = { tagMap: null, users: [], expiresAt: 0 }

async function refreshCache() {
  if (Date.now() < cache.expiresAt) return cache

  const [tagsRes, usersRes] = await Promise.all([
    fetch(`${BASE}/pages/${PAGE_ID}/tags?page_access_token=${TOKEN}`),
    fetch(`${BASE}/pages/${PAGE_ID}/users?page_access_token=${TOKEN}`),
  ])
  const { tags = [] } = await tagsRes.json()
  const { users = [] } = await usersRes.json()

  cache = {
    tagMap: Object.fromEntries(tags.map((t) => [t.text.toLowerCase(), String(t.id)])),
    users: users.filter((u) => u.status === 'available'),
    expiresAt: Date.now() + 60 * 60 * 1000,
  }
  return cache
}

// Webhook handler — Pancake gọi vào mỗi khi có message mới
export default async function pancakeWebhook(req, res) {
  res.status(200).json({ ok: true })   // Trả 200 ngay tránh Pancake retry — xử lý async

  const event = req.body
  if (event.type !== 'new_message') return
  if (event.from?.id === event.page_id) return  // skip tin nhắn từ page tự gửi (tránh loop)

  const { conversation_id, message, page_id } = event
  const { tagMap, users } = await refreshCache()

  // 1. Auto-tag theo keyword
  const text = (message?.text || '').toLowerCase()
  const tagIds = []
  if (text.match(/mua|đặt|order/)) tagIds.push(tagMap['đặt hàng'])
  if (text.match(/giá|price/)) tagIds.push(tagMap['hỏi giá'])
  if (text.match(/khiếu nại|complaint/)) tagIds.push(tagMap['khiếu nại'])

  for (const tagId of tagIds.filter(Boolean)) {
    await fetch(
      `${BASE}/pages/${page_id}/conversations/${conversation_id}/tags?page_access_token=${TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'add', tag_id: tagId }),
      }
    )
    await new Promise((r) => setTimeout(r, 250))
  }

  // 2. Auto-assign round-robin (nếu chưa assign)
  if (!event.assignee_ids || event.assignee_ids.length === 0) {
    const nextUser = users[Math.floor(Math.random() * users.length)]   // simple random
    if (nextUser) {
      await fetch(
        `${BASE}/pages/${page_id}/conversations/${conversation_id}/assign?page_access_token=${TOKEN}`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ assignee_ids: [nextUser.id] }),
        }
      )
    }
  }
}

IMPORTANT

Pattern quan trọng: trả 200 OK trước, xử lý sau. Pancake có timeout webhook (vài giây) — nếu xử lý tag/assign chậm rồi mới trả response, Pancake sẽ retry → duplicate side-effects. Tốt nhất: push event vào queue (Redis/SQS), worker xử lý async. Code trên là phiên bản đơn giản hoá cho minh hoạ.


9. Pitfalls thường gặp

Triệu chứngNguyên nhânCách fix
Tag không hiện trong dashboard sau khi addBrowser cache, hoặc lookup nhầm tag_idF5 dashboard; verify tag_id từ GET /tags
400 khi assignuser.id sai (lấy fb_id thay vì id)Dùng field id (UUID) từ GET /users, không phải fb_id
Assign rồi nhân viên không nhận notiUser status = offline / disabledFilter users theo status='available' && status_in_page='active' trước khi assign
Add tag thành công nhưng data array thiếu tag mớiTag đã có sẵn từ trước, hoặc race conditionĐọc data để xác nhận state thực tế thay vì giả định
Webhook bị Pancake retry vô hạnHandler chậm/timeout/trả 5xxTrả 200 ngay, xử lý async; check rate limit; log error

10. FAQ

Q: tag_id lấy từ đâu? Có thể dùng tag name trực tiếp không?

A: Lấy từ GET /pages/{page_id}/tags. Không thể dùng tag name — phải lookup ID trước. Khuyến nghị cache map name→id trong memory.

Q: Có thể tạo tag mới qua API không?

A: Không. Tag chỉ tạo được từ dashboard Pancake (Cài đặt → Nhãn). API chỉ cho phép add/remove tag đã tồn tại.

Q: Assign nhiều người 1 lúc được không?

A: Có. assignee_ids là mảng — truyền nhiều UUID. Tất cả sẽ thấy conversation trong inbox của mình. Use case: hội thoại quan trọng cần 2 người cùng theo dõi.

Q: Làm sao remove tất cả tag của conversation cùng lúc?

A: Không có endpoint bulk remove. Phải GET conversation → lấy tags array → loop POST remove từng cái với throttle.

Q: Pancake có tự assign khi khách mới nhắn vào không?

A: nếu bạn config round-robin users trong dashboard hoặc qua POST /round_robin_users. Pancake auto-assign cho user tiếp theo theo lượt. Không cần tự code — xem section 5 TIPtài liệu chính thức Pancake về Phân chia hội thoại tự động.

Q: Khi nhân viên offline, conversation auto-assign cho người tiếp theo không?

A: Pancake round-robin built-in check status — skip user offline. Tự code phải tự filter users.filter(u => u.is_online && u.status === 'available').

Q: Tích hợp với Salesforce/HubSpot — mapping tag Pancake → pipeline stage như nào?

A: Tạo map tĩnh trong code:

  • Pancake tag "Hỏi giá" → HubSpot stage "Qualified Lead"
  • Pancake tag "Đặt hàng" → HubSpot stage "Closed Won"
  • Pancake tag "Khiếu nại" → Salesforce Case "New"

Mỗi lần Pancake webhook fire conversation_tagged event → call HubSpot/Salesforce API update stage. Tham khảo bài 3 phương án truy xuất data cho integration pattern chi tiết.


11. Tổng kết

  • 4 endpoint workflow: tags (add/remove), assign (assignee_ids array), read, unread. Tất cả ở public_api/v1, page_access_token.
  • 2 endpoint lookup bắt buộc: GET /tags (lấy tag_id), GET /users (lấy user.id UUID).
  • Pancake có round-robin built-in — dùng nếu logic assign đơn giản, đỡ phải code.
  • Auto-tag pattern: webhook → keyword match → POST add tag. Throttle dưới 5 req/s.
  • Assign replace toàn bộ, không append — phải merge trước khi POST.
  • Trả 200 webhook trước, xử lý async sau — tránh Pancake retry gây duplicate.

Bài viết liên quan


Last updated: 2026-06-09. Phiên bản API: public_api/v1 cho toàn bộ endpoint workflow. Tham khảo OpenAPI spec để verify thay đổi.