Citely Docs
Nhà phát triển

Ký & xác minh webhook

Citely ký mọi request bằng Standard Webhooks (HMAC-SHA256). Cách receiver xác minh.

Mọi GET/POST Citely gửi tới receiver đều được ký theo chuẩn mở Standard Webhooks (HMAC-SHA256) bằng CITELY_WEBHOOK_SECRET. Receiver verify chữ ký trên POST trước khi lưu bất cứ gì.

Headers trên mỗi delivery

webhook-id:        msg_8b1f…        (idempotency key — dedupe trên cái này)
webhook-timestamp: 1751270400       (UNIX seconds — reject nếu lệch > 5 phút)
webhook-signature: v1,K5l8…         (danh sách "v1,<base64>" cách nhau bởi space)
content-type:      application/json
user-agent:        Citely/1.0 (+https://citely-seo.com)

Thuật toán verify

id        = header "webhook-id"
ts        = header "webhook-timestamp"   (UNIX seconds)
sigHeader = header "webhook-signature"   (danh sách "v1,<base64>" cách nhau bởi space)

if !id || !ts || abs(now_seconds - ts) > 300:  reject (401 CPP_REPLAY)
key           = base64_decode( secret sau khi bỏ tiền tố "whsec_" )
signedContent = id + "." + ts + "." + rawBody
expected      = base64( HMAC_SHA256(key, signedContent) )
# tách sigHeader theo space; mỗi token lấy phần SAU dấu phẩy; so sánh constant-time.
# BẤT KỲ token nào khớp → hợp lệ. (Danh sách cho phép xoay secret.)

Ký trên RAW body (bytes nguyên văn). Nếu bạn JSON.parse rồi stringify lại để ký, whitespace/thứ tự khoá đổi → chữ ký sai. Đọc raw trước, verify, rồi mới parse.

Reference (Node/TypeScript, zero-dependency)

import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';

function verify(secret: string, body: string, h: Record<string, string>): boolean {
  const id = h['webhook-id'];
  const ts = h['webhook-timestamp'];
  const sig = h['webhook-signature'] ?? '';
  if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return false;
  }
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const exp = crypto
    .createHmac('sha256', key)
    .update(`${id}.${ts}.${body}`)
    .digest('base64');
  const eb = Buffer.from(exp, 'base64');
  return sig.split(' ').some((t) => {
    const s = Buffer.from(t.includes(',') ? t.slice(t.indexOf(',') + 1) : t, 'base64');
    return s.length === eb.length && crypto.timingSafeEqual(s, eb);
  });
}

Off-the-shelf tương đương:

npm i standardwebhooks
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.CITELY_WEBHOOK_SECRET!);
wh.verify(rawBody, headers); // throw nếu chữ ký/timestamp sai

Xoay secret

Header webhook-signaturedanh sách chữ ký (cách nhau bởi space). Khi bạn xoay secret trong Citely, một giai đoạn ngắn Citely gửi nhiều chữ ký — receiver coi là hợp lệ nếu bất kỳ token nào khớp. Nhờ vậy bạn xoay secret không downtime.

Sai chữ ký → trả 401 CPP_SIG_INVALID. Lệch đồng hồ > 300s → 401 CPP_REPLAY. Xem Mã lỗi.

On this page