# Webhook

MoreLogin có thể POST một sự kiện JSON tới endpoint HTTPS do bạn quản lý ngay khi một thao tác cloud phone hoặc cloud browser có kết quả cuối cùng, nên bạn không còn phải polling để biết kết quả. Mỗi nhóm chỉ lưu một URL callback, và Local API cùng Open API ghi vào cùng một bản ghi.

## Cấu hình URL callback

Cả hai bề mặt đều cung cấp cùng một thao tác. Open API:

```bash
curl -X POST "https://api.morelogin.com/webhook/config" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Local API:

```bash
curl -X POST "http://127.0.0.1:40000/api/webhook/config" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Route của Local API do client máy tính phục vụ, nên cần client MoreLogin v2.66.0 trở lên. Endpoint của Open API không phụ thuộc client.

`teamId` không bao giờ được đọc từ request. Mỗi lần gọi đều cấu hình cho nhóm thuộc ngữ cảnh phía máy chủ, nên bạn không thể trỏ sự kiện của nhóm khác về endpoint của mình.

URL phải là HTTPS, dài từ 1 đến 1024 ký tự, có tên host, không chứa thông tin người dùng, không có fragment và cổng hợp lệ. Mọi địa chỉ mà nó phân giải ra đều phải định tuyến được trên internet: các dải localhost, riêng tư, link-local, CGNAT, metadata đám mây, dải dành riêng cho giao thức, dải dùng cho tài liệu và 6to4 sẽ bị từ chối với mã [`41001`](/vi/api-reference/getting-started/error-codes). Cùng phép kiểm tra đó được thực hiện lại khi một lần gửi mở kết nối, nên một tên host sau này phân giải vào dải riêng tư chỉ làm lần gửi đó thất bại chứ không chạm tới mạng của bạn.

Việc lưu là upsert theo nhóm, nên gửi cùng một body hai lần cũng không gây hại.

### Tắt việc gửi

`enabled: false` giữ lại URL đã lưu và dừng tạo sự kiện mới. Các sự kiện đã nằm trong hàng đợi sẽ bị hủy lần lượt, vào đúng thời điểm mỗi sự kiện sắp được cấp phép, và chỉ khi cấu hình vẫn đang tắt — nên nếu bạn bật lại sớm, các sự kiện trong hàng đợi sẽ tiếp tục được gửi thay vì bị loại bỏ.

## Một lần gửi trông như thế nào

Mỗi lần gửi là một `POST` với các header sau:

| Header | Giá trị |
|  --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `MoreLogin-Webhook/1.0` |
| `X-Webhook-Event-Id` | Giống `eventId` trong body |
| `X-Webhook-Event-Type` | Giống `eventType` trong body |


Hai header `X-Webhook-*` cố ý trùng với các trường trong body, để router hoặc queue có thể phân phối theo chúng mà không cần phân tích JSON.

```json
{
  "eventId": "c7f3a2b10d8e4f6a9c1b2d3e4f5a6b7c",
  "eventType": "cloud_phone.power_off",
  "eventTime": "1787241600000",
  "data": {
    "teamId": "10001",
    "cloudPhoneId": "20001",
    "reason": "MONEY_SAVING"
  },
  "result": {
    "success": true,
    "code": "0",
    "message": null
  }
}
```

| Trường | Mô tả |
|  --- | --- |
| `eventId` | ID sự kiện duy nhất. Các lần thử lại và phát lại nội bộ của cùng sự kiện đều dùng lại nó, nên đây là khóa idempotency |
| `eventType` | Tên sự kiện ổn định, lấy từ bảng dưới |
| `eventTime` | Thời điểm kết quả cuối cùng xảy ra, tính bằng milli giây epoch Unix. **Được gửi dưới dạng chuỗi** |
| `data.teamId` | Nhóm sở hữu tài nguyên. **Được gửi dưới dạng chuỗi** |
| `data.cloudPhoneId` | Cloud phone mà sự kiện nói về. **Được gửi dưới dạng chuỗi** |
| `data.cloudBrowserId` | Profile cloud browser mà sự kiện nói về. **Được gửi dưới dạng chuỗi** |
| `data.runId` | Phiên chạy cloud browser mà sự kiện nói về |
| `data.reason` | Chỉ có ở `cloud_phone.power_off` và `cloud_browser.stop` |
| `result.success` | Thao tác cuối cùng có thành công hay không |
| `result.code` | `0` khi thành công; nếu không thì là mã lỗi nghiệp vụ ổn định, không phụ thuộc ngôn ngữ |
| `result.message` | `null` khi thành công; nếu không thì là phần giải thích theo ngôn ngữ của nhóm, quay về `en-US` khi thiếu |


Mọi định danh dạng số và `eventTime` đều đến dưới dạng chuỗi JSON, giống phần còn lại của API — xem [Định dạng phản hồi chung](/vi/api-reference/getting-started/response-format). Hãy phân tích chúng như chuỗi thay vì số, nếu không bạn sẽ mất độ chính xác với ID 64-bit.

## Sự kiện

| `eventType` | Kết quả cuối cùng mà nó báo |
|  --- | --- |
| `cloud_phone.power_on` | Bật thành công hoặc thất bại |
| `cloud_phone.power_off` | Tắt thành công. Lần tắt thất bại không tạo sự kiện |
| `cloud_phone.restart` | Khởi động lại thành công hoặc thất bại |
| `cloud_phone.reset` | Đặt lại thành công hoặc thất bại |
| `cloud_phone.new_machine` | Tạo máy mới một lần bấm thành công hoặc thất bại |
| `cloud_browser.start` | Mở cloud browser thành công hoặc thất bại |
| `cloud_browser.stop` | Mỗi lần thử đóng cloud browser, dù thành công hay không |


Sự kiện cloud phone mang theo `teamId` và `cloudPhoneId`. Sự kiện cloud browser mang theo `teamId`, `cloudBrowserId` và `runId`.

Bật, khởi động lại, đặt lại và tạo máy mới một lần bấm chỉ báo `success: true` sau khi callback của nhà cung cấp thành công và việc đồng bộ cục bộ đã commit. Lỗi tính phí khi bật, hoặc một lần kiểm tra proxy bất đồng bộ thất bại và hoàn tất dọn dẹp, đều báo là bật thất bại và **không** tạo thêm sự kiện tắt. Các việc hoãn lại như cài ứng dụng mặc định hay xóa tệp đã hết hạn không làm thay đổi kết quả.

Nếu một lần tắt làm gián đoạn quá trình bật, lần thử đó không tạo sự kiện bật; lần tắt thành công sau đó vẫn tạo `cloud_phone.power_off` riêng.

### Lý do dừng

`cloud_phone.power_off` bổ sung `data.reason`:

| Giá trị | Ý nghĩa |
|  --- | --- |
| `NORMAL` | Tắt thông thường |
| `MONEY_SAVING` | Chế độ tiết kiệm chi phí |
| `OTHER` | Nợ phí, hết hạn, bảo trì hoặc lý do khác |


`cloud_browser.stop` bổ sung `data.reason`:

| Giá trị | Ý nghĩa |
|  --- | --- |
| `NORMAL` | Đóng thông thường |
| `MONEY_SAVING` | Chế độ tiết kiệm chi phí |
| `ENERGY_SAVING` | Chế độ tiết kiệm năng lượng |
| `BROWSER_EXITED` | Trình duyệt thoát ngoài dự kiến |
| `OTHER` | Lý do khác |


Lỗi lưu trữ nội bộ chỉ ảnh hưởng tới `result`; nó không bao giờ ghi đè `reason` đã chốt khi bắt đầu đóng.

## Gửi, thử lại và thứ tự

Mọi mã `2xx` đều tính là đã gửi thành công. Mọi trạng thái khác, cùng với timeout kết nối, timeout đọc và lỗi mạng, đều tính là thất bại. Timeout kết nối là 5 giây và timeout đọc là 30 giây.

Một lần gửi thất bại sẽ được thử lại tối đa 6 lần, sau 10 giây, 30 giây, 60 giây, 3 phút, 10 phút và 30 phút. Tức là tối đa 7 request cho một sự kiện. Sau khi lần thử lại thứ sáu thất bại, sự kiện sẽ ngừng được thử lại tự động.

Sự kiện của cùng một cloud phone, hoặc cùng một cloud browser, được gửi theo thứ tự tạo, mỗi lần một sự kiện. Các tài nguyên khác nhau được gửi song song, nên đừng giả định có thứ tự nào giữa chúng.

Các lần thử lại của một sự kiện luôn dùng lại cùng `eventId`, `eventTime` và body. Trong các tình huống biên về mạng hoặc phục hồi, cùng một sự kiện có thể được gửi nhiều lần với các giá trị đó không đổi, nên hãy **loại trùng theo `eventId`**.

Với việc đóng cloud browser, một lần thử đóng tạo ra đúng một sự kiện. Lần thử lại thủ công sau đó, lần giải phóng cưỡng bức thủ công, hoặc lần giải phóng cưỡng bức do hệ thống khởi tạo đều là thao tác mới và nhận `eventId` mới. Hai lần thử không bao giờ bị gộp thành một sự kiện.

Các thao tác cloud phone được nhận diện qua ID tác vụ của nhà cung cấp, `traceId`, hoặc một mốc thời gian nghiệp vụ ổn định. Khi không có cái nào, dịch vụ sinh một UUID và coi callback cuối cùng hiện tại là thao tác mới, nên bạn nhận được `eventId` mới thay vì hai thao tác khác nhau bị gộp thành một.

## Viết phía nhận

Hãy trả về `2xx` ngay khi bạn đã lưu sự kiện, và xử lý thật sự theo cách bất đồng bộ. Body phản hồi bị bỏ qua, và một handler chậm sẽ tiêu hết 30 giây đọc của bạn rồi đẩy sự kiện vào lịch thử lại dù bạn đã nhận được nó.

Hãy loại trùng theo `eventId`. Coi một lần trùng là đã xử lý thay vì chạy lại các tác dụng phụ.

Đừng coi payload là dữ liệu đáng tin chỉ vì nó đã tới endpoint của bạn. Bản phát hành này không ký callback, nên bất kỳ ai biết URL của bạn đều có thể gửi tới đó. Hãy dùng URL khó đoán, và trước khi làm việc gì tốn kém, hãy xác nhận trạng thái qua API — ví dụ [`POST /cloudphone/info`](/vi/api-reference/cloud-phone/open-api) cho cloud phone, hoặc `POST /cloudbrowser/page` cho một phiên chạy cloud browser.

`result.code` dùng cùng bộ mã nghiệp vụ với API đồng bộ, nên có thể tra một lần thất bại trong [Mã lỗi](/vi/api-reference/getting-started/error-codes). `result.message` là chuỗi dành cho người đọc, câu chữ và ngôn ngữ đều có thể thay đổi; hãy rẽ nhánh theo `result.success` và `result.code`, đừng bao giờ theo `result.message`.

Hợp đồng máy đọc được của từng sự kiện, gồm schema và ví dụ cho từng sự kiện, được công bố cùng đặc tả shared resources tại [Shared Resources Open API](/vi/api-reference/shared-resources/open-api).