Theo dõi thay đổi: feed + webhooks
CrawlPipe cung cấp hai cách để biết khi nào một văn bản (hoặc một khoản cụ thể bên trong) thay đổi: truy vấn luồng thay đổi (changes feed), hoặc đăng ký nhận webhooks. Feed luôn là nguồn sự thật — webhooks là phương thức tối ưu độ trễ dựa trên feed.
Luồng thay đổi (Changes feed)
GET /v1/legal/changes (1 đơn vị, gói Builder trở lên) là nhật ký sự kiện chỉ ghi thêm (append-only), phân trang theo cursor được sắp xếp tăng dần theo id.
curl "https://api.crawlpipe.com/v1/legal/changes?since=0&limit=100" \
-H "Authorization: Bearer cp_live_a1b2c3d4e5f6…"import requests
cursor = "0" # 0 = từ đầu, phát lại toàn bộ lịch sử
resp = requests.get(
"https://api.crawlpipe.com/v1/legal/changes",
params={"since": cursor, "limit": 100},
headers={"Authorization": "Bearer cp_live_a1b2c3d4e5f6…"},
)
body = resp.json()
events = body["data"]["events"]
next_cursor = body["meta"]["pagination"]["next_cursor"]let cursor = "0";
const res = await fetch(
`https://api.crawlpipe.com/v1/legal/changes?since=${cursor}&limit=100`,
{ headers: { Authorization: "Bearer cp_live_a1b2c3d4e5f6…" } },
);
const { data, meta } = await res.json();
cursor = meta.pagination.next_cursor ?? cursor;Lưu trữ meta.pagination.next_cursor và truyền lại làm tham số since trong lần truy vấn tiếp theo. since=0 phát lại toàn bộ lịch sử từ đầu (được phân trang) — dùng một lần để đổ dữ liệu ban đầu, sau đó chuyển sang truy vấn tăng tiến bằng cursor gần nhất.
Mỗi sự kiện gồm:
{
"id": 12345,
"type": "record.updated",
"subtype": "units_amended",
"occurred_at": "2026-08-01T04:12:09Z",
"record": { "id": "…", "natural_key": "vn:tt:2026:18:btc", "doc_number": "18/2026/TT-BTC", "title": "…" },
"affected_units": ["d3.k2"],
"diff": { "…": "structured unit-level diff" }
}diff có cấu trúc (các đường dẫn đơn vị thêm/xóa/sửa và delta metadata), không phải diff văn bản thô — đủ nhỏ để giữ trong feed. Để đọc văn bản mới, hãy gọi GET /v1/legal/documents/{id} cho các đường dẫn trong affected_units.
Webhooks
Tạo đăng ký một lần (trên giao diện portal hoặc POST /v1/webhooks), và CrawlPipe sẽ gửi các sự kiện khớp tới endpoint của bạn ngay khi chúng xảy ra — thường trong vòng vài giây kể từ khi có mục nhập trong feed.
POST /v1/webhooks
{
"url": "https://example.com/hooks/crawlpipe",
"domain": "legal-vn",
"event_types": ["record.updated"],
"filters": { "doc_types": ["thong_tu"], "fields": ["thue"] }
}Phản hồi bao gồm secret (whsec_…) duy nhất một lần — hãy lưu lại để xác minh các thông báo gửi đến.
Nội dung thông báo (Payload)
{
"id": "evt_000000012345",
"type": "record.updated",
"subtype": "units_amended",
"domain": "legal-vn",
"occurred_at": "2026-08-01T04:12:09Z",
"data": {
"record_id": "0198…",
"natural_key": "vn:tt:2026:18:btc",
"doc_number": "18/2026/TT-BTC",
"affected_units": ["d3.k2"],
"diff": { "…": "…" },
"links": { "document": "https://api.crawlpipe.com/v1/legal/documents/0198…" }
}
}Xác minh X-CrawlPipe-Signature
Mỗi lượt gửi đều chứa X-CrawlPipe-Signature: t={unix_ts},v1={hex_hmac} trong đó hex_hmac = hex(hmac_sha256(secret, "{t}.{raw_body}")). Hãy tính lại và so sánh thời gian hằng số (constant time); đồng thời từ chối các thông báo có độ lệch đồng hồ quá 5 phút (chống tấn công phát lại).
# chỉ mang tính minh họa — việc xác minh diễn ra trong mã máy chủ của bạn
echo -n "${t}.${body}" | openssl dgst -sha256 -hmac "${secret}"import hashlib
import hmac
import time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = int(parts["t"]), parts["v1"]
if abs(time.time() - ts) > 300:
return False
expected = hmac.new(
secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret: string, header: string, rawBody: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const ts = Number(parts.t);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Cam kết dịch vụ
- Ít nhất một lần (At-least-once) — khử trùng lặp dựa trên
X-CrawlPipe-Delivery(ID chuyển giao) hoặcidcủa payload. - Thử lại theo thời gian giãn cách 1m → 5m → 30m → 2h → 12h → 24h (6 lần thử) trước khi đánh dấu là
dead; đăng ký sẽ tự động bịdisablesau 20 lần thất bại liên tiếp (kèm email thông báo). - Đăng ký mới không nhận lịch sử phát lại — chỉ các sự kiện tạo ra sau khi đăng ký tồn tại mới đủ điều kiện. Hãy nạp lịch sử từ
/changestrước (since=0), sau đó tin tưởng vào webhooks cho tương lai. - Thứ tự gửi từng đăng ký không được đảm bảo tuyệt đối khi thử lại. Nếu tích hợp của bạn cần thứ tự nghiêm ngặt (như đồng bộ kế toán), hãy truy vấn
/changesbằng cursor thay vì phụ thuộc vào thứ tự nhận webhook.