Appearance
Webhooks
🛠 Dành cho IT / Lập trình viên
Trang này hướng đến bộ phận IT hoặc lập trình viên trong doanh nghiệp của bạn. Nếu bạn chỉ dùng app bình thường, có thể bỏ qua mục này.
Webhooks cho phép CNV Work gửi sự kiện realtime đến URL của bạn ngay khi có thay đổi: lead mới, deal chốt thắng, tin nhắn đến, đơn duyệt... Không cần polling API liên tục — hệ thống đẩy event tới bạn ngay khi xảy ra.
Truy cập
Vào menu Cài đặt > Webhooks trên thanh sidebar, hoặc truy cập trực tiếp https://hub.cnvwork.com/webhooks.
Lưu ý
Chỉ vai trò Owner và Admin mới có quyền tạo, chỉnh sửa và xoá webhook.
Khi nào dùng Webhook
| Tình huống | Cách phù hợp |
|---|---|
| Cần biết khi có lead mới để bot phân loại tự động | Webhook lead.created |
| Đồng bộ deal sang Data Warehouse mỗi 5 phút | Polling API (cron job) |
| Bắn Slack notification khi deal chốt thắng | Webhook deal.won |
| Gửi SMS cho khách khi đơn được duyệt | Webhook approval.approved |
| Báo cáo tổng hợp cuối ngày | Polling API + scheduled job |
Webhook = push (CNV → bạn). API = pull (bạn → CNV). Kết hợp cả hai cho luồng phức tạp: webhook báo có event, app của bạn gọi ngược API để lấy chi tiết.
Tổng quan luồng
Tạo Webhook
- Mở trang Webhooks
- Nhấn + Tạo webhook
- Điền thông tin:
| Trường | Mô tả | Bắt buộc |
|---|---|---|
| Tên | Tên nhận biết (VD: "Slack notification") | Có |
| URL | URL nhận POST (HTTPS bắt buộc) | Có |
| Secret | Khoá ký HMAC (auto-generate hoặc tự nhập) | Có |
| Events | Danh sách event muốn nhận (xem bên dưới) | Có |
| Mô tả | Ghi chú | Không |
| Headers tuỳ biến | Header bổ sung gửi kèm (VD: API key của bạn) | Không |
| Trạng thái | Active / Paused | Mặc định Active |
- Nhấn Lưu
- Hệ thống gửi ngay 1 test ping đến URL để kiểm tra. Nếu URL trả
2xx→ webhook active. Nếu lỗi → vẫn lưu nhưng đánh dấuunverified
Mẹo
Trong giai đoạn dev, dùng webhook.site hoặc ngrok để kiểm tra payload trước khi cắm vào server thật.
Danh sách event
CRM
| Event | Khi nào trigger |
|---|---|
lead.created | Lead mới được tạo |
lead.updated | Cập nhật bất kỳ trường nào của lead |
lead.assigned | Lead được gán owner |
lead.converted | Lead chuyển thành contact + deal |
deal.created | Deal mới |
deal.updated | Cập nhật deal |
deal.stage_changed | Chuyển stage trong pipeline |
deal.won | Chuyển sang stage "Thắng" |
deal.lost | Chuyển sang stage "Thua" |
contact.created | Contact mới |
contact.updated | Cập nhật contact |
company.created | Công ty mới |
quotation.sent | Báo giá gửi đi |
quotation.accepted | Khách đồng ý báo giá |
Inbox
| Event | Khi nào trigger |
|---|---|
message.received | Tin nhắn mới từ khách (inbound) |
message.sent | Tin nhắn gửi đi (outbound) |
conversation.assigned | Hội thoại được gán cho nhân viên |
conversation.closed | Đóng hội thoại |
conversation.reopened | Mở lại hội thoại |
Ticket
| Event | Khi nào trigger |
|---|---|
ticket.created | Ticket mới |
ticket.assigned | Gán ticket cho nhân viên |
ticket.status_changed | Đổi trạng thái (open/pending/resolved/closed) |
ticket.commented | Có comment mới |
ticket.sla_breached | Vượt SLA cam kết |
Approval
| Event | Khi nào trigger |
|---|---|
approval.requested | Tạo đơn duyệt mới |
approval.approved | Đơn được duyệt (mọi bước) |
approval.rejected | Đơn bị từ chối |
approval.cancelled | Người tạo huỷ đơn |
approval.step_completed | Hoàn thành 1 bước trong workflow nhiều bước |
HR
| Event | Khi nào trigger |
|---|---|
employee.created | Nhân viên mới được thêm |
employee.terminated | Nhân viên nghỉ việc |
leave.requested | Đơn nghỉ phép mới |
leave.approved | Đơn nghỉ phép được duyệt |
Format payload
Mọi event đều có cấu trúc chung:
json
{
"id": "evt_01HXY7ZK4M8PRWQVT3JN6BFGCD",
"event": "lead.created",
"occurred_at": "2026-05-26T08:42:15.123Z",
"workspace_id": "ws_abc123",
"api_version": "v1",
"data": {
"id": "lead_xyz789",
"name": "Nguyễn Văn A",
"phone": "0901234567",
"email": "anguyen@example.com",
"source": "website",
"status": "new",
"owner_id": null,
"created_at": "2026-05-26T08:42:15.000Z",
"custom_fields": {
"utm_source": "google",
"utm_campaign": "spring-sale"
}
}
}Trường data chứa snapshot của entity tại thời điểm event. Với event *.updated, có thêm changes:
json
{
"event": "deal.stage_changed",
"occurred_at": "2026-05-26T09:15:00.000Z",
"workspace_id": "ws_abc123",
"data": {
"id": "deal_aaa111",
"name": "Đơn Acme Corp",
"stage": "negotiation",
"amount": 50000000,
"owner_id": "user_bbb222"
},
"changes": {
"stage": { "from": "presentation", "to": "negotiation" }
}
}Headers gửi kèm
CNV Work POST request kèm các header:
http
POST /your/webhook/endpoint HTTP/1.1
Host: yourapp.com
Content-Type: application/json
User-Agent: CNVWork-Webhook/1.0
X-CNV-Event: lead.created
X-CNV-Delivery: evt_01HXY7ZK4M8PRWQVT3JN6BFGCD
X-CNV-Workspace: ws_abc123
X-CNV-Timestamp: 1714128135
X-CNV-Signature: sha256=a3f5b8c2...
X-CNV-Attempt: 1| Header | Ý nghĩa |
|---|---|
X-CNV-Event | Tên event (giúp route nhanh không cần parse body) |
X-CNV-Delivery | ID lần delivery (dùng để dedupe) |
X-CNV-Workspace | Workspace gửi event |
X-CNV-Timestamp | Unix timestamp khi sign |
X-CNV-Signature | HMAC-SHA256 của body |
X-CNV-Attempt | Lần thử thứ mấy (1-5) |
Verify chữ ký HMAC
Bắt buộc
Luôn verify signature trước khi xử lý payload — nếu không, kẻ tấn công có thể giả mạo event và gây thiệt hại nghiêm trọng (tạo đơn giả, trigger workflow trái phép).
Signature = sha256= + HMAC-SHA256(secret, raw_request_body) (hex).
Lưu ý: phải dùng raw body (chưa parse JSON), nếu parse rồi stringify lại có thể khác bytes → signature không khớp.
Node.js (Express)
js
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.CNV_WEBHOOK_SECRET;
// Lấy raw body trước khi parse JSON
app.use('/webhook/cnv', express.raw({ type: 'application/json' }));
function verifySignature(rawBody, signatureHeader, secret) {
if (!signatureHeader) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// timingSafeEqual chống timing attack
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
app.post('/webhook/cnv', (req, res) => {
const signature = req.headers['x-cnv-signature'];
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log(`Received ${event.event} for workspace ${event.workspace_id}`);
// Route theo event type
switch (event.event) {
case 'lead.created':
handleLeadCreated(event.data);
break;
case 'deal.won':
handleDealWon(event.data);
break;
default:
console.log('Unhandled event:', event.event);
}
// Phải trả 2xx trong 10s, nếu logic chậm thì queue async
res.status(200).send('ok');
});
function handleLeadCreated(lead) {
// ... gửi Slack, sync CRM khác, v.v
}
function handleDealWon(deal) {
// ... bắn noti, tính commission, v.v
}
app.listen(3000, () => console.log('Webhook listener on :3000'));Python (Flask)
python
import os
import hmac
import hashlib
import json
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['CNV_WEBHOOK_SECRET']
def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not signature_header:
return False
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256,
).hexdigest()
# compare_digest chống timing attack
return hmac.compare_digest(expected, signature_header)
@app.route('/webhook/cnv', methods=['POST'])
def cnv_webhook():
raw_body = request.get_data()
signature = request.headers.get('X-CNV-Signature', '')
if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
abort(401, 'invalid signature')
event = json.loads(raw_body.decode('utf-8'))
print(f"Received {event['event']} for workspace {event['workspace_id']}")
handlers = {
'lead.created': handle_lead_created,
'deal.won': handle_deal_won,
}
handler = handlers.get(event['event'])
if handler:
handler(event['data'])
return 'ok', 200
def handle_lead_created(lead):
# ... gửi Slack, sync CRM khác
pass
def handle_deal_won(deal):
# ... bắn noti, tính commission
pass
if __name__ == '__main__':
app.run(port=3000)Retry policy
Nếu URL của bạn trả về 5xx hoặc timeout > 10s hoặc không phản hồi, hệ thống retry tự động:
| Lần thử | Sau bao lâu |
|---|---|
| 1 | Ngay lập tức |
| 2 | +1 phút |
| 3 | +5 phút |
| 4 | +30 phút |
| 5 | +2 giờ |
| 6 | +12 giờ |
Sau 6 lần fail liên tiếp, hệ thống tự động Pause webhook và gửi email cảnh báo cho Owner. Phải bật lại thủ công tại trang Webhook detail.
Response 4xx (trừ 408, 429) coi là lỗi vĩnh viễn → không retry (vì có nghĩa endpoint refuse có chủ đích).
Mẹo
Endpoint phải idempotent: cùng 1 X-CNV-Delivery ID có thể đến nhiều lần (do retry). Lưu danh sách delivery ID đã xử lý 24h gần nhất, skip nếu đã thấy.
Test webhook
Tại trang Webhook detail, tab Test:
- Chọn event type (VD:
lead.created) - Hệ thống hiển thị payload mẫu — bạn chỉnh sửa nếu muốn
- Nhấn Gửi test
- Kết quả hiển thị: status code, response body, latency
- Tab Logs ghi nhận như delivery thật
Logs
Mở Webhook detail > tab Logs xem 200 delivery gần nhất:
| Cột | Mô tả |
|---|---|
| Thời gian | Khi gửi |
| Event | Loại event |
| Status | Response code (200, 500, timeout...) |
| Attempt | Lần thử thứ mấy |
| Latency | Thời gian endpoint phản hồi (ms) |
| Hành động | Xem payload / Resend / Replay |
Nhấn Resend để gửi lại delivery cụ thể (giữ nguyên X-CNV-Delivery ID — endpoint nên dedupe).
Nhấn Replay để gửi lại nhưng tạo X-CNV-Delivery mới (coi như event hoàn toàn mới).
Trạng thái webhook
| Trạng thái | Ý nghĩa |
|---|---|
| Active | Đang chạy, nhận event |
| Paused | Tạm dừng (user pause hoặc auto-pause sau 6 fail) |
| Unverified | Test ping chưa thành công lần nào |
| Disabled | Bị Owner disable, không trigger nữa |
Best practice
Khuyến nghị
- Luôn verify HMAC signature — không bao giờ skip dù chạy localhost
- Trả 2xx nhanh (<5s) — push event vào queue nội bộ, xử lý async sau
- Idempotent: dedupe theo
X-CNV-DeliveryID - HTTPS bắt buộc — không hỗ trợ HTTP plain
- Rotate secret mỗi 90 ngày: đổi secret trong CNV Work → đổi env var bên bạn → restart service
- Subscribe ít event: chỉ chọn event thực sự cần, tránh quá tải endpoint
- Monitor: alert nếu webhook bị auto-pause hoặc tỷ lệ fail > 5%
- Versioning: gắn
X-CNV-Webhook-Version: v1header tuỳ biến để dễ migrate khi có v2
Lỗi thường gặp
| Lỗi | Nguyên nhân | Xử lý |
|---|---|---|
| Webhook auto-paused | Endpoint fail 6 lần liên tiếp | Sửa lỗi server, manual Resume |
| Signature không khớp | Parse JSON rồi stringify lại | Dùng raw body bytes |
| Delivery trùng lặp | Retry sau timeout | Dedupe theo X-CNV-Delivery |
| Endpoint timeout | Logic chậm | Push vào queue, xử lý async |
| Không nhận được event | Status Paused hoặc URL sai | Kiểm tra trạng thái + URL |
Liên kết liên quan
- API Keys — gọi ngược API để lấy chi tiết khi nhận event
- Marketplace & Mini App — xây Mini App tích hợp UI
