Skip to content

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ò OwnerAdmin mới có quyền tạo, chỉnh sửa và xoá webhook.

Khi nào dùng Webhook

Tình huốngCách phù hợp
Cần biết khi có lead mới để bot phân loại tự độngWebhook lead.created
Đồng bộ deal sang Data Warehouse mỗi 5 phútPolling API (cron job)
Bắn Slack notification khi deal chốt thắngWebhook deal.won
Gửi SMS cho khách khi đơn được duyệtWebhook approval.approved
Báo cáo tổng hợp cuối ngàyPolling 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

  1. Mở trang Webhooks
  2. Nhấn + Tạo webhook
  3. Điền thông tin:
TrườngMô tảBắt buộc
TênTên nhận biết (VD: "Slack notification")
URLURL nhận POST (HTTPS bắt buộc)
SecretKhoá ký HMAC (auto-generate hoặc tự nhập)
EventsDanh sách event muốn nhận (xem bên dưới)
Mô tảGhi chúKhông
Headers tuỳ biếnHeader bổ sung gửi kèm (VD: API key của bạn)Không
Trạng tháiActive / PausedMặc định Active
  1. Nhấn Lưu
  2. 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ấu unverified

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

EventKhi nào trigger
lead.createdLead mới được tạo
lead.updatedCập nhật bất kỳ trường nào của lead
lead.assignedLead được gán owner
lead.convertedLead chuyển thành contact + deal
deal.createdDeal mới
deal.updatedCập nhật deal
deal.stage_changedChuyển stage trong pipeline
deal.wonChuyển sang stage "Thắng"
deal.lostChuyển sang stage "Thua"
contact.createdContact mới
contact.updatedCập nhật contact
company.createdCông ty mới
quotation.sentBáo giá gửi đi
quotation.acceptedKhách đồng ý báo giá

Inbox

EventKhi nào trigger
message.receivedTin nhắn mới từ khách (inbound)
message.sentTin nhắn gửi đi (outbound)
conversation.assignedHội thoại được gán cho nhân viên
conversation.closedĐóng hội thoại
conversation.reopenedMở lại hội thoại

Ticket

EventKhi nào trigger
ticket.createdTicket mới
ticket.assignedGán ticket cho nhân viên
ticket.status_changedĐổi trạng thái (open/pending/resolved/closed)
ticket.commentedCó comment mới
ticket.sla_breachedVượt SLA cam kết

Approval

EventKhi nào trigger
approval.requestedTạo đơn duyệt mới
approval.approvedĐơn được duyệt (mọi bước)
approval.rejectedĐơn bị từ chối
approval.cancelledNgười tạo huỷ đơn
approval.step_completedHoàn thành 1 bước trong workflow nhiều bước

HR

EventKhi nào trigger
employee.createdNhân viên mới được thêm
employee.terminatedNhâ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-EventTên event (giúp route nhanh không cần parse body)
X-CNV-DeliveryID lần delivery (dùng để dedupe)
X-CNV-WorkspaceWorkspace gửi event
X-CNV-TimestampUnix timestamp khi sign
X-CNV-SignatureHMAC-SHA256 của body
X-CNV-AttemptLầ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
1Ngay 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:

  1. Chọn event type (VD: lead.created)
  2. Hệ thống hiển thị payload mẫu — bạn chỉnh sửa nếu muốn
  3. Nhấn Gửi test
  4. Kết quả hiển thị: status code, response body, latency
  5. Tab Logs ghi nhận như delivery thật

Logs

Mở Webhook detail > tab Logs xem 200 delivery gần nhất:

CộtMô tả
Thời gianKhi gửi
EventLoại event
StatusResponse code (200, 500, timeout...)
AttemptLần thử thứ mấy
LatencyThời gian endpoint phản hồi (ms)
Hành độngXem 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
PausedTạm dừng (user pause hoặc auto-pause sau 6 fail)
UnverifiedTest ping chưa thành công lần nào
DisabledBị Owner disable, không trigger nữa

Best practice

Khuyến nghị

  1. Luôn verify HMAC signature — không bao giờ skip dù chạy localhost
  2. Trả 2xx nhanh (<5s) — push event vào queue nội bộ, xử lý async sau
  3. Idempotent: dedupe theo X-CNV-Delivery ID
  4. HTTPS bắt buộc — không hỗ trợ HTTP plain
  5. Rotate secret mỗi 90 ngày: đổi secret trong CNV Work → đổi env var bên bạn → restart service
  6. Subscribe ít event: chỉ chọn event thực sự cần, tránh quá tải endpoint
  7. Monitor: alert nếu webhook bị auto-pause hoặc tỷ lệ fail > 5%
  8. Versioning: gắn X-CNV-Webhook-Version: v1 header tuỳ biến để dễ migrate khi có v2

Lỗi thường gặp

LỗiNguyên nhânXử lý
Webhook auto-pausedEndpoint fail 6 lần liên tiếpSửa lỗi server, manual Resume
Signature không khớpParse JSON rồi stringify lạiDùng raw body bytes
Delivery trùng lặpRetry sau timeoutDedupe theo X-CNV-Delivery
Endpoint timeoutLogic chậmPush vào queue, xử lý async
Không nhận được eventStatus Paused hoặc URL saiKiểm tra trạng thái + URL

Liên kết liên quan

CNV Work — Nền tảng SaaS đa workspace cho doanh nghiệp