Webhook
Nextfarm gửi POST tới hệ thống của bạn khi có sự kiện ở trại. Chủ trại thêm địa chỉ nhận (https://) và chọn sự kiện ở Hệ thống › Webhook.
Sự kiện
| Sự kiện | Nhóm | Khi nào |
|---|---|---|
iot.alert.raised | IoT | Cảm biến vượt ngưỡng (nhiệt độ, độ ẩm, EC…). Có thiết bị, chỉ số, giá trị, ngưỡng, mức độ. |
iot.device.offline | IoT | Thiết bị ngừng báo về quá thời gian cho phép. |
harvest.batch.created | Thu hoạch | Một bản ghi thu hoạch được khai báo và xác nhận: thửa, loại cây, sản lượng (kg). |
harvest.batch.verified | Thu hoạch | Cán bộ xác minh lô thu hoạch: sản lượng thực, hạng chất lượng, ngày thu. |
harvest.qr.created | Truy xuất | Một tem QR truy xuất nguồn gốc vừa được tạo cho lô thu hoạch hoặc mùa vụ. |
gis.season.created | Mùa vụ | Một mùa vụ được tạo trên thửa: cây trồng, tên vụ, ngày trồng, ngày dự kiến thu. |
gis.season.closed | Mùa vụ | Một mùa vụ được đóng: ngày đóng, sản lượng (kg) nếu có. |
gis.parcel.created | Thửa | Một thửa mới được thêm vào trại: vùng, chủ hộ, cây trồng, diện tích (ha). |
office.task.completed | Công việc | Một việc trong trại được báo xong: tên việc, người làm, lúc xong, thửa/mùa vụ gắn kèm. |
Tin gửi đi
POST https://erp.htx-cua-ban.vn/hooks/nextfarm
Content-Type: application/json
X-Nextfarm-Event: iot.alert.raised
X-Nextfarm-Delivery: 0199... (mã lượt giao — dùng để bỏ trùng)
X-Nextfarm-Timestamp: 1790467200 (giây Unix)
X-Nextfarm-Signature: sha256=9c1e...
webhook-id: 0199...
webhook-timestamp: 1790467200
webhook-signature: v1,K5oZfz...
{
"id": "0199...", "type": "iot.alert.raised",
"workspace_id": "...", "occurred_at": "2026-09-27T03:10:00Z",
"data": { "alert_id": "...", "device_id": "...", "device_code": "NK2", "zone_id": "...",
"parcel_ids": [], "metric": "temperature", "value": 41.2, "threshold": 38,
"severity": "high", "message": "..." }
}iot.alert.raisedIoTscope: iot.alerts.viewCảnh báo cảm biến mới
Cảm biến vượt ngưỡng (nhiệt độ, độ ẩm, EC…). Có thiết bị, chỉ số, giá trị, ngưỡng, mức độ.
Ví dụ data
{
"alert_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"device_code": "NK2",
"device_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"message": "Nhiệt độ nhà kính 2 vượt 38°C",
"metric": "temperature",
"parcel_ids": [],
"severity": "high",
"threshold": 38,
"value": 41.2,
"zone_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003"
}JSON Schema
{
"properties": {
"alert_id": {
"format": "uuid",
"type": "string"
},
"device_code": {
"type": "string"
},
"device_id": {
"format": "uuid",
"type": "string"
},
"message": {
"type": "string"
},
"metric": {
"type": "string"
},
"parcel_ids": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"severity": {
"type": [
"string",
"null"
]
},
"threshold": {
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
"type": [
"string",
"number"
]
},
"value": {
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$",
"type": [
"string",
"number"
]
},
"zone_id": {
"format": "uuid",
"type": "string"
}
},
"required": [
"alert_id",
"device_id",
"device_code",
"zone_id",
"parcel_ids",
"metric",
"value",
"threshold",
"severity",
"message"
],
"type": "object"
}iot.device.offlineIoTscope: iot.devices.viewThiết bị mất kết nối
Thiết bị ngừng báo về quá thời gian cho phép.
Ví dụ data
{
"device_code": "NK2",
"device_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"device_name": "Bộ tưới nhà kính 2",
"last_heartbeat": "2026-10-28T06:45:00+00:00",
"parcel_ids": [],
"zone_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003"
}JSON Schema
{
"properties": {
"device_code": {
"type": "string"
},
"device_id": {
"format": "uuid",
"type": "string"
},
"device_name": {
"type": "string"
},
"last_heartbeat": {
"format": "date-time",
"type": [
"string",
"null"
]
},
"parcel_ids": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"zone_id": {
"format": "uuid",
"type": "string"
}
},
"required": [
"device_id",
"device_code",
"device_name",
"zone_id",
"parcel_ids",
"last_heartbeat"
],
"type": "object"
}harvest.batch.createdThu hoạchscope: harvest.harvest.viewLô thu hoạch được xác nhận
Một bản ghi thu hoạch được khai báo và xác nhận: thửa, loại cây, sản lượng (kg).
Ví dụ data
{
"confirmed_at": "2026-10-28T07:30:00+00:00",
"crop_type_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003",
"harvest_record_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"parcel_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"quantity_kg": 1250.5
}JSON Schema
{
"properties": {
"confirmed_at": {
"format": "date-time",
"type": "string"
},
"crop_type_id": {
"format": "uuid",
"type": "string"
},
"harvest_record_id": {
"format": "uuid",
"type": "string"
},
"parcel_id": {
"format": "uuid",
"type": "string"
},
"quantity_kg": {
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
"type": [
"string",
"number"
]
}
},
"required": [
"harvest_record_id",
"parcel_id",
"crop_type_id",
"quantity_kg",
"confirmed_at"
],
"type": "object"
}harvest.batch.verifiedThu hoạchscope: harvest.harvest.viewLô thu hoạch được xác minh
Cán bộ xác minh lô thu hoạch: sản lượng thực, hạng chất lượng, ngày thu.
Ví dụ data
{
"actual_quantity_kg": 1198,
"crop_type_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003",
"crop_type_name": "Vải thiều",
"harvest_date": "2026-10-27",
"harvest_record_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"parcel_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"quality_grade": "A",
"season_id": null,
"verified_at": "2026-10-28T07:30:00+00:00"
}JSON Schema
{
"properties": {
"actual_quantity_kg": {
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
"type": [
"string",
"number"
]
},
"crop_type_id": {
"format": "uuid",
"type": "string"
},
"crop_type_name": {
"type": [
"string",
"null"
]
},
"harvest_date": {
"format": "date",
"type": "string"
},
"harvest_record_id": {
"format": "uuid",
"type": "string"
},
"parcel_id": {
"format": "uuid",
"type": "string"
},
"quality_grade": {
"type": [
"string",
"null"
]
},
"season_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"verified_at": {
"format": "date-time",
"type": "string"
}
},
"required": [
"harvest_record_id",
"parcel_id",
"season_id",
"crop_type_id",
"crop_type_name",
"actual_quantity_kg",
"quality_grade",
"harvest_date",
"verified_at"
],
"type": "object"
}harvest.qr.createdTruy xuấtscope: harvest.qr-codes.viewTem truy xuất được tạo
Một tem QR truy xuất nguồn gốc vừa được tạo cho lô thu hoạch hoặc mùa vụ.
Ví dụ data
{
"code": "NF-7Q2K-91XD",
"created_at": "2026-10-28T07:30:00+00:00",
"harvest_record_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"qr_code_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"season_id": null
}JSON Schema
{
"properties": {
"code": {
"type": "string"
},
"created_at": {
"format": "date-time",
"type": "string"
},
"harvest_record_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"qr_code_id": {
"format": "uuid",
"type": "string"
},
"season_id": {
"format": "uuid",
"type": [
"string",
"null"
]
}
},
"required": [
"qr_code_id",
"code",
"season_id",
"harvest_record_id",
"created_at"
],
"type": "object"
}gis.season.createdMùa vụscope: gis.parcels.viewMùa vụ mới
Một mùa vụ được tạo trên thửa: cây trồng, tên vụ, ngày trồng, ngày dự kiến thu.
Ví dụ data
{
"branch_id": null,
"crop_name": "Lúa",
"crop_type_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003",
"expected_harvest_on": "2027-02-20",
"parcel_code": "A-12",
"parcel_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"planted_on": "2026-11-05",
"season_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"season_name": "Đông Xuân 2026"
}JSON Schema
{
"properties": {
"branch_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"crop_name": {
"type": "string"
},
"crop_type_id": {
"format": "uuid",
"type": "string"
},
"expected_harvest_on": {
"format": "date",
"type": [
"string",
"null"
]
},
"parcel_code": {
"type": "string"
},
"parcel_id": {
"format": "uuid",
"type": "string"
},
"planted_on": {
"format": "date",
"type": "string"
},
"season_id": {
"format": "uuid",
"type": "string"
},
"season_name": {
"type": "string"
}
},
"required": [
"season_id",
"season_name",
"parcel_id",
"parcel_code",
"branch_id",
"crop_type_id",
"crop_name",
"planted_on",
"expected_harvest_on"
],
"type": "object"
}gis.season.closedMùa vụscope: gis.parcels.viewMùa vụ kết thúc
Một mùa vụ được đóng: ngày đóng, sản lượng (kg) nếu có.
Ví dụ data
{
"closed_at": "2026-10-28T07:30:00+00:00",
"closed_on": "2027-02-21",
"season_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"yield_kg": 5400
}JSON Schema
{
"properties": {
"closed_at": {
"format": "date-time",
"type": "string"
},
"closed_on": {
"format": "date",
"type": "string"
},
"season_id": {
"format": "uuid",
"type": "string"
},
"yield_kg": {
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
"type": [
"string",
"number",
"null"
]
}
},
"required": [
"season_id",
"closed_on",
"yield_kg",
"closed_at"
],
"type": "object"
}gis.parcel.createdThửascope: gis.parcels.viewThửa mới
Một thửa mới được thêm vào trại: vùng, chủ hộ, cây trồng, diện tích (ha).
Ví dụ data
{
"area_ha": 1.25,
"created_at": "2026-10-28T07:30:00+00:00",
"crop_type_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003",
"owner_id": null,
"parcel_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"zone_id": "0190a3c2-5b1e-7c3d-8e4f-000000000002"
}JSON Schema
{
"properties": {
"area_ha": {
"pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
"type": [
"string",
"number"
]
},
"created_at": {
"format": "date-time",
"type": "string"
},
"crop_type_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"owner_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"parcel_id": {
"format": "uuid",
"type": "string"
},
"zone_id": {
"format": "uuid",
"type": "string"
}
},
"required": [
"parcel_id",
"zone_id",
"owner_id",
"crop_type_id",
"area_ha",
"created_at"
],
"type": "object"
}office.task.completedCông việcscope: office.task.viewViệc hoàn thành
Một việc trong trại được báo xong: tên việc, người làm, lúc xong, thửa/mùa vụ gắn kèm.
Ví dụ data
{
"branch_id": null,
"completed_at": "2026-10-28T07:30:00+00:00",
"completed_by": "0190a3c2-5b1e-7c3d-8e4f-000000000002",
"parcel_id": "0190a3c2-5b1e-7c3d-8e4f-000000000003",
"season_id": null,
"task_id": "0190a3c2-5b1e-7c3d-8e4f-000000000001",
"title": "Bón thúc đợt 2 thửa A-12"
}JSON Schema
{
"properties": {
"branch_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"completed_at": {
"format": "date-time",
"type": "string"
},
"completed_by": {
"format": "uuid",
"type": "string"
},
"parcel_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"season_id": {
"format": "uuid",
"type": [
"string",
"null"
]
},
"task_id": {
"format": "uuid",
"type": "string"
},
"title": {
"type": "string"
}
},
"required": [
"task_id",
"title",
"completed_by",
"completed_at",
"branch_id",
"parcel_id",
"season_id"
],
"type": "object"
}Verify signature
Cách nhanh nhất: Standard Webhooks
Mỗi tin còn kèm 3 header theo chuẩn mở Standard Webhooks: webhook-id (= X-Nextfarm-Delivery), webhook-timestamp, webhook-signature: v1,…. Dùng thư viện standardwebhooks (Node, Python, Go, Java, C#, PHP, Ruby…) với đúng secret whsec_… — thư viện tự verify signature và độ lệch giờ. Áp dụng cho mọi địa chỉ, kể cả địa chỉ tạo trước 14/10/2026.
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.NEXTFARM_WEBHOOK_SECRET); // "whsec_..."
app.post("/hooks/nextfarm", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = wh.verify(req.body, req.headers); // ném lỗi / throws if invalid
res.sendStatus(200);
} catch {
res.sendStatus(401);
}
});from standardwebhooks.webhooks import Webhook
wh = Webhook(SECRET) # "whsec_..."
@app.post("/hooks/nextfarm")
def hook():
try:
event = wh.verify(request.get_data(), dict(request.headers))
except Exception:
return "", 401
return "", 200Hoặc tự kiểm X-Nextfarm-Signature:
X-Nextfarm-Signature = sha256= + hex của HMAC-SHA256(bí_mật, timestamp + "." + thân_tin). Secret (whsec_…) hiện một lần lúc tạo địa chỉ hoặc lúc xoay. Từ chối tin có timestamp lệch quá 5 phút.
import crypto from "node:crypto";
// cần thân tin NGUYÊN BẢN — express.raw, không express.json
app.post("/hooks/nextfarm", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-Nextfarm-Timestamp");
const expected = "sha256=" + crypto.createHmac("sha256", process.env.NEXTFARM_WEBHOOK_SECRET)
.update(ts + "." + req.body).digest("hex");
const got = req.header("X-Nextfarm-Signature") ?? "";
const ok = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
if (!ok || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const event = JSON.parse(req.body); // bỏ trùng theo X-Nextfarm-Delivery
res.sendStatus(200);
});import hmac, hashlib, time
@app.post("/hooks/nextfarm")
def hook():
ts = request.headers["X-Nextfarm-Timestamp"]
body = request.get_data()
expected = "sha256=" + hmac.new(SECRET.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Nextfarm-Signature", "")) \
or abs(time.time() - int(ts)) > 300:
return "", 401
return "", 200Retry và tạm dừng
- Trả
2xxtrong 10 giây là thành công. Mã khác, quá giờ hoặc chuyển hướng (3xx) là hỏng. - Hỏng thì retry sau 1 phút, 5 phút, 30 phút, 2 giờ, 6 giờ, 24 giờ (tổng 7 lần gửi).
- 5 lượt giao hỏng hết mọi lần thử, liên tiếp ⇒ địa chỉ tự tạm dừng; bật lại ở màn Webhook.
- Màn Webhook có Gửi thử (tin
webhook.test), nhật ký giao 50 lượt gần nhất, Giao lại, Xoay secret — secret cũ ký kèm thêm 24 giờ (X-Nextfarm-Signature-Previous+ signature thứ hai trongwebhook-signature).
https:// tới máy chủ công khai. Nextfarm từ chối địa chỉ nội bộ (localhost, 10.x, 192.168.x, 169.254.x…) cả lúc lưu lẫn lúc gửi.Webhooks của app (cho đối tác)
Khác webhook của trại ở trên: đây là tin NextFarm gửi cho ứng dụng của bạn — MỘT Callback URL nhận sự kiện của mọi trại đã cài (thân tin có workspace_id). Luôn có app.* khi trại cài, gỡ, đổi scope; bật thêm sự kiện dữ liệu (cùng tên, cùng thân với webhook của trại) — chỉ gửi cho trại ĐÃ CẤP scope tương ứng. Khai ở Ứng dụng của tôi › app › Webhooks; ký Standard Webhooks bằng signing secret whsec_ riêng của app (tạo mới thì bản cũ ký kèm 24 giờ) — kiểm bằng thư viện standardwebhooks. Hỏng thì retry 1′, 5′, 30′, 2h, 6h, 24h; 5 sự kiện hỏng liên tiếp ⇒ tạm dừng + thư.
| type | |
|---|---|
app.installed | Một trại vừa cho phép app (lần đầu hoặc cài lại). |
app.scopes_changed | Trại sửa scope đã cấp — token cũ bị chặn, xin token mới. |
app.uninstalled | Trại gỡ app — token của trại đó bị chặn ngay. |
app.ping | Tin thử (nút Gửi thử, và lúc chạy Publish checklist). |
harvest.batch.created | Sự kiện dữ liệu — trại xác nhận lô thu hoạch (cần harvest.harvest.view). |
iot.alert.raised | Sự kiện dữ liệu — cảm biến vượt ngưỡng (cần iot.alerts.view). |
iot.device.offline | Sự kiện dữ liệu — thiết bị mất kết nối (cần iot.devices.view). |
{ "id": "0199…", "type": "app.installed", "created_at": "2026-10-14T09:12:00Z",
"data": { "client_id": "nfa_8f3c…", "workspace_id": "019d…3c1",
"workspace_name": "Trại Ninh", "scopes": ["harvest.harvest.view"] } }Luật bảo mật đầy đủ cho đối tác: Bảo mật →