Sự kiện

Webhooks và sự kiện

Đăng ký, kiểm chữ ký, thử lại, danh sách sự kiện.

Đăng ký

Console › Webhooks › Thêm địa chỉ nhận, theo từng môi trường. Hoặc qua API với token ứng dụng và quyền webhooks:manage.

Một địa chỉ nhận phục vụ mọi kết nối của ứng dụng. Mỗi sự kiện mang connection_id và organization_id để bạn biết của khách nào. Khoá bí mật whsec_… chỉ hiện một lần.

Môi trường chính thức bắt buộc https:// và không chấp nhận địa chỉ mạng nội bộ. Môi trường thử nghiệm nhận http:// để bạn thử qua tunnel.

Định dạng

POST {url}
Content-Type: application/json
X-CTAS-Event: identity.confirmed
X-CTAS-Event-Id: evt_01J9…-whk_…
X-CTAS-Timestamp: 1757127672
X-CTAS-Signature: t=1757127672,v1=5f1a…
X-CTAS-Api-Version: 2026-09-01

Body là sự kiện đầy đủ:

{ "id": "evt_01J9…", "type": "member.created", "api_version": "2026-09-01",
  "created_at": "…", "connection_id": "con_…", "organization_id": "org_…",
  "data": { … } }

Kiểm chữ ký

Bắt buộc. Tính HMAC-SHA256 với khoá bí mật của địa chỉ nhận trên chuỗi "{timestamp}.{raw body}". Từ chối nếu lệch giờ quá 300 giây. So sánh bằng hàm thời gian hằng. Dùng raw body, đừng parse rồi stringify lại.

import crypto from 'node:crypto'

export function verifyCtas(rawBody, headers, secret) {
  const ts = headers['x-ctas-timestamp']
  const sig = Object.fromEntries(headers['x-ctas-signature'].split(',').map(p => p.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
  const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1 ?? ''))
}
function verifyCtas(string $rawBody, array $headers, string $secret): bool {
    $ts = (int) ($headers['X-CTAS-Timestamp'] ?? 0);
    if (abs(time() - $ts) > 300) return false;
    $parts = [];
    foreach (explode(',', $headers['X-CTAS-Signature'] ?? '') as $p) {
        [$k, $v] = explode('=', $p, 2) + [null, null];
        $parts[$k] = $v;
    }
    return hash_equals(hash_hmac('sha256', "{$ts}.{$rawBody}", $secret), (string) ($parts['v1'] ?? ''));
}

Bấm Gửi thử trong console để nhận một sự kiện webhook.test ngay và xem kết quả. Đây là cách nhanh nhất kiểm tra chữ ký trước khi nối thật.

Trả lời và thử lại

  • Trả 2xx trong 8 giây. Xử lý nặng thì đưa vào hàng đợi rồi trả 200.
  • Không nhận được 2xx (hết giờ, 5xx, 408, 429): CTAS gửi lại sau 1 giây, 5 giây, 30 giây, 2 phút, 10 phút. Sau 5 lần thì đánh dấu thất bại, bạn gửi lại được trong console.
  • 4xx khác: không thử lại.
  • Thất bại 20 lần liên tiếp: địa chỉ nhận bị tắt, chủ ứng dụng nhận email.
  • X-CTAS-Event-Id không đổi giữa các lần gửi. Hãy khử trùng theo id này. Thứ tự sự kiện không đảm bảo.

Danh sách sự kiện

Sự kiệnKhi nào
connection.created, connection.revokedtổ chức kết nối, ngắt kết nối
connection.compromisedrefresh token cũ bị dùng lại, mọi token của kết nối đã bị thu hồi
identity.confirmed, identity.declined, identity.expiredxác nhận bằng QR
member.created, member.updated, member.leftnhân sự thay đổi, từ ứng dụng hay từ API
member.avatar_updatednhân sự đổi hoặc gỡ ảnh đại diện; chỉ có member_id và has_avatar
attendance.checked_in, attendance.checked_outchấm công
leave.requested, leave.decidedđơn nghỉ
process.submission.created, process.submission.reviewed, process.occurrence.missedquy trình
payslip.closed, payslip.reopened, payslip.verified, payslip.unverified, payslip.paid, payslip.deletedphiếu lương đổi bước; chỉ có payslip_id, member_id, period, state — đọc số bằng payroll:read

Không dùng webhook

GET /v1/events?since=&type=&connection=&cursor= trả cùng nội dung theo thứ tự thời gian, giữ 30 ngày. Token ứng dụng thấy mọi sự kiện của môi trường. Token kết nối chỉ thấy sự kiện của kết nối đó.

TrườngGiá trị
deliveries.statuspending, sent, failed