← Tài liệu

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ệnNhómKhi nào
iot.alert.raisedIoTCả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.offlineIoTThiết bị ngừng báo về quá thời gian cho phép.
harvest.batch.createdThu hoạchMộ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.verifiedThu hoạchCá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.createdTruy xuấtMộ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.createdMù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.closedMùa vụMột mùa vụ được đóng: ngày đóng, sản lượng (kg) nếu có.
gis.parcel.createdThửaMộ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.completedCông việcMộ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

Lệnh mẫu
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.view

Cả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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Thiế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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Lô 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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Lô 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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Tem 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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Mù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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Mù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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Thử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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.view

Việ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

Lệnh mẫu
{
  "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
Lệnh mẫu
{
  "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.

Node.js — npm i standardwebhooks
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);
  }
});
Python — pip install standardwebhooks
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 "", 200

Hoặ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.

Node.js (Express)
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);
});
Python (Flask)
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 "", 200

Retry và tạm dừng

  • Trả 2xx trong 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 trong webhook-signature).
Địa chỉ nhận phải là 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.installedMột trại vừa cho phép app (lần đầu hoặc cài lại).
app.scopes_changedTrại sửa scope đã cấp — token cũ bị chặn, xin token mới.
app.uninstalledTrại gỡ app — token của trại đó bị chặn ngay.
app.pingTin thử (nút Gửi thử, và lúc chạy Publish checklist).
harvest.batch.createdSự kiện dữ liệu — trại xác nhận lô thu hoạch (cần harvest.harvest.view).
iot.alert.raisedSự kiện dữ liệu — cảm biến vượt ngưỡng (cần iot.alerts.view).
iot.device.offlineSự kiện dữ liệu — thiết bị mất kết nối (cần iot.devices.view).
app.installed
{ "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 →