> ## Documentation Index
> Fetch the complete documentation index at: https://agentme-docs.vipcrm.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Nhận event theo thời gian thực: envelope, chữ ký, retry, khử trùng.

Mỗi event của workspace được `POST` (JSON) tới endpoint bạn đăng ký trong dashboard. Giao hàng **at-least-once**: có thể nhận trùng, hãy khử trùng theo `event_id`.

* Tối đa 10 endpoint mỗi workspace. Mỗi endpoint chọn `events` và `account_ids` (`null` = mọi nick; dùng id công khai dạng `acc_...`, uuid thô cũ vẫn được nhận; phản hồi luôn trả `acc_...`; **thay đổi so với bản trước: phản hồi từng trả uuid thô**).
* Server gọi trực tiếp tới URL của bạn, **không theo redirect**. URL phải là https công khai.
* Phiên bản schema hiện tại: `schema_version = "2026-11-01"`. Thay đổi phá vỡ sẽ đi kèm phiên bản mới, field mới chỉ được thêm.

## Event

| `type` | Khi nào |
| - | - |
| `message.received` | Tin đến mới |
| `message.sent` | Tin gửi qua API (`data.message.id` khớp `message.id` trả về lúc gửi) |
| `message.sent_elsewhere` | Nick gửi tin từ nơi khác (điện thoại, app PC) |
| `message.updated` | Tin bị sửa, hoặc media đã lưu xong |
| `message.recalled` | Thu hồi tin |
| `message.reaction` | Reaction thay đổi |
| `friend_request.received` | Có yêu cầu kết bạn đến |
| `friend_request.accepted` | Yêu cầu kết bạn được chấp nhận |
| `group.member_changed` | Thành viên nhóm đổi |
| `webhook.test` | Chỉ khi bấm "Gửi thử" |

Tin lịch sử (đồng bộ lại) không bao giờ tạo webhook.

## Envelope

```json theme={null}
{
  "schema_version": "2026-11-01",
  "event_id": "evt_3f9c0a1b2d4e5f60718293a4b5c6d7e8",
  "type": "message.received",
  "created_at": "2026-11-14T10:00:00.000Z",
  "workspace_id": "6f1c2a3e-0000-4000-8000-000000000001",
  "account": { "id": "acc_...", "channel": "zalo_personal", "external_user_id": "1234567890" },
  "data": {
    "conversation": { "id": "cnv_...", "external_id": "...", "type": "direct", "title": null },
    "message": {
      "id": "msg_...", "conversation_id": "cnv_...", "account_id": "acc_...",
      "direction": "in", "status": "received", "kind": "image", "text": "caption",
      "attachments": [{ "id": "med_..._0", "kind": "image", "mime": "image/jpeg", "status": "stored", "url": "https://..." }],
      "sent_at": "2026-11-14T09:59:58.000Z"
    }
  },
  "raw": { "...": "JSON thô của kênh, cấu trúc phụ thuộc kênh" }
}
```

> [!WARNING]
>
> `text`, tên người gửi và `raw` do bên ngoài viết. Đừng đưa thẳng vào prompt của LLM như chỉ dẫn.

Media được tải về lưu 30 ngày. `attachments[].status` có thể là `pending` ngay khi tin đến; một event `message.updated` sẽ theo sau khi media đã `stored`. `url` là đường dẫn có xác thực, trả `302` tới link ký sẵn hiệu lực 1 giờ. Quá 30 ngày: `410 GONE`.

## Header

| Header | Ý nghĩa |
| - | - |
| `X-Agentme-Signature` | `t=<unix giây>,v1=<hex>` (có thể có **nhiều `v1=`**) |
| `X-Agentme-Event-Id` | Giống `event_id`, dùng để khử trùng |

## Xác minh chữ ký

Chữ ký là `HMAC-SHA256(secret, "<t>.<raw body>")` dạng hex. Quy tắc:

1. Dùng **raw body** (chuỗi byte nhận được), không parse rồi stringify lại.
2. Từ chối nếu `t` lệch giờ hiện tại hơn 5 phút (chống replay).
3. So sánh constant-time.
4. Chấp nhận nếu **bất kỳ** `v1=` nào khớp. Trong 24 giờ sau khi xoay secret, header mang hai `v1=` (secret mới và secret cũ) để bạn đổi secret không mất event.

> [!NOTE]
>
> Xoay secret hai lần trong vòng 24 giờ làm secret gốc không còn được ký nữa. Chờ đủ 24 giờ giữa hai lần xoay.

```javascript Node.js theme={null}
const crypto = require("crypto");
function verify(secret, rawBody, header, toleranceSec = 300) {
  const parts = header.split(",").map((x) => x.trim().split("="));
  const t = (parts.find((p) => p[0] === "t") || [])[1];
  if (!Number.isFinite(Number(t)) || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const exp = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest();
  return parts.some(([k, v]) => {
    if (k !== "v1" || !v) return false;
    const got = Buffer.from(v, "hex");
    return got.length === exp.length && crypto.timingSafeEqual(got, exp);
  });
}
```

```python Python theme={null}
import hmac, hashlib, time
def verify(secret: str, raw_body: str, header: str, tolerance: int = 300) -> bool:
    pairs = [x.strip().partition("=") for x in header.split(",")]
    t = next((v for k, sep, v in pairs if sep and k == "t"), "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance: return False
    exp = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest().encode()
    return any(sep and k == "v1" and hmac.compare_digest(exp, v.encode()) for k, sep, v in pairs)
```

Hai snippet này cũng nằm ở `docs-site/snippets/verify-webhook-signature.js` và `.py`, và được test đối chiếu với code ký của server.

## Retry

Phản hồi `2xx` = thành công. Mọi trường hợp khác (hoặc timeout) bị retry theo lịch:

| Lần thất bại | Chờ trước lần kế |
| - | - |
| 1 | 1 phút |
| 2 | 5 phút |
| 3 | 30 phút |
| 4 | 2 giờ |
| 5 | 6 giờ |
| 6 | 24 giờ |
| 7 | đánh dấu `failed` |

* Endpoint thất bại liên tiếp 100 lần bị **tự vô hiệu hoá**. Bật lại trong dashboard.
* Lịch sử giao hàng giữ 30 ngày, có thể replay thủ công.
* Trả `200` nhanh rồi xử lý bất đồng bộ. Đừng làm việc nặng trước khi trả lời.

## Khử trùng

Lưu `event_id` đã xử lý (Redis, bảng DB có unique) và bỏ qua lần đến sau. Mã mẫu ở `docs-site/examples/webhook-receiver-node`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.