Webhooks
Nextfarm sends a POST to your system when something happens on a farm. The farm owner adds a receiving URL (https://) and picks events in System › Webhooks.
Events
| Event | Group | When |
|---|---|---|
iot.alert.raised | IoT | A sensor crossed a threshold (temperature, humidity, EC…): device, metric, value, threshold, severity. |
iot.device.offline | IoT | A device stopped reporting for longer than allowed. |
harvest.batch.created | Harvest | A harvest record was declared and confirmed: parcel, crop type, yield (kg). |
harvest.batch.verified | Harvest | A harvest batch was verified: actual yield, quality grade, harvest date. |
harvest.qr.created | Traceability | A traceability QR label was created for a harvest batch or season. |
gis.season.created | Seasons | A season was created on a parcel: crop, season name, planting date, expected harvest date. |
gis.season.closed | Seasons | A season was closed: closing date and yield (kg) if recorded. |
gis.parcel.created | Parcels | A new parcel was added to the farm: zone, owner, crop and area (ha). |
office.task.completed | Tasks | A farm task was marked done: title, who completed it, when, and the linked parcel/season. |
Outgoing message
POST https://erp.your-company.com/hooks/nextfarm
Content-Type: application/json
X-Nextfarm-Event: iot.alert.raised
X-Nextfarm-Delivery: 0199... (delivery ID — use it to de-duplicate)
X-Nextfarm-Timestamp: 1790467200 (Unix seconds)
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.viewNew sensor alert
A sensor crossed a threshold (temperature, humidity, EC…): device, metric, value, threshold, severity.
Example 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.viewDevice offline
A device stopped reporting for longer than allowed.
Example 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.createdHarvestscope: harvest.harvest.viewHarvest batch confirmed
A harvest record was declared and confirmed: parcel, crop type, yield (kg).
Example 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.verifiedHarvestscope: harvest.harvest.viewHarvest batch verified
A harvest batch was verified: actual yield, quality grade, harvest date.
Example 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.createdTraceabilityscope: harvest.qr-codes.viewTraceability QR created
A traceability QR label was created for a harvest batch or season.
Example 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.createdSeasonsscope: gis.parcels.viewSeason created
A season was created on a parcel: crop, season name, planting date, expected harvest date.
Example 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.closedSeasonsscope: gis.parcels.viewSeason closed
A season was closed: closing date and yield (kg) if recorded.
Example 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.createdParcelsscope: gis.parcels.viewParcel created
A new parcel was added to the farm: zone, owner, crop and area (ha).
Example 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.completedTasksscope: office.task.viewTask completed
A farm task was marked done: title, who completed it, when, and the linked parcel/season.
Example 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"
}Verifying the signature
Fastest: Standard Webhooks
Every message also carries 3 headers from the open Standard Webhooks spec: webhook-id (= X-Nextfarm-Delivery), webhook-timestamp, webhook-signature: v1,…. Use the standardwebhooks library (Node, Python, Go, Java, C#, PHP, Ruby…) with your whsec_… secret as-is — it checks the signature and clock skew for you. Applies to every URL, including ones created before 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 "", 200Or verify X-Nextfarm-Signature yourself:
X-Nextfarm-Signature = sha256= + hex of HMAC-SHA256(secret, timestamp + "." + body). The secret (whsec_…) is shown once, when the URL is created or rotated. Reject messages whose timestamp is off by more than 5 minutes.
import crypto from "node:crypto";
// needs the RAW body — express.raw, not 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); // de-duplicate on 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 "", 200Retries and pausing
- Returning
2xxwithin 10 seconds counts as success. Any other code, a timeout or a redirect (3xx) counts as a failure. - Failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours (7 attempts in total).
- 5 consecutive deliveries that fail every attempt ⇒ the URL is paused automatically; re-enable it on the Webhooks screen.
- The Webhooks screen has Send test (a
webhook.testmessage), a log of the last 50 deliveries, Redeliver and Rotate secret — the old secret co-signs for 24 more hours (X-Nextfarm-Signature-Previous+ a second signature inwebhook-signature).
https:// to a public server. Nextfarm rejects private addresses (localhost, 10.x, 192.168.x, 169.254.x…) both when saving and when sending.App Webhooks (for partners)
Different from the farm webhooks above: these are messages NextFarm sends to your app — ONE Callback URL receives events from every installed farm (the body carries workspace_id). app.* events always arrive when a farm installs, removes or changes scopes; you can also turn on data events (same names and bodies as farm webhooks) — sent only for farms that GRANTED the matching scope. Configure them in My apps › app › Webhooks; signed per Standard Webhooks with the app's own whsec_ signing secret (after rotation the old one co-signs for 24 hours) — verify with the standardwebhooks library. Failures retry after 1′, 5′, 30′, 2h, 6h, 24h; 5 consecutive failed events ⇒ paused + an email.
| type | |
|---|---|
app.installed | A farm just allowed the app (first time or reinstall). |
app.scopes_changed | The farm changed the granted scopes — old tokens are blocked, request a new one. |
app.uninstalled | The farm removed the app — its tokens are blocked immediately. |
app.ping | Test message (Send test button, and during the Publish checklist). |
harvest.batch.created | Data event — a farm confirms a harvest batch (needs harvest.harvest.view). |
iot.alert.raised | Data event — a sensor crosses a threshold (needs iot.alerts.view). |
iot.device.offline | Data event — a device goes offline (needs 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"] } }Full security rules for partners: Security →