← Docs
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 Manage › Webhooks.
Events
| Event | Group | When |
|---|---|---|
iot.alert.raised | IoT | A sensor crossed a threshold: device, metric, value, threshold, severity. |
iot.device.offline | IoT | A device stopped reporting for longer than allowed. |
harvest.batch.created | Harvest | A harvest batch was declared and confirmed: parcel, crop type, yield (kg). |
Outgoing message
Example
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...
{
"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": "..." }
}| Event | Fields in data |
|---|---|
iot.alert.raised | alert_id, device_id, device_code, zone_id, parcel_ids, metric, value, threshold, severity, message |
iot.device.offline | device_id, device_code, device_name, zone_id, parcel_ids, last_heartbeat |
harvest.batch.created | harvest_record_id, parcel_id, crop_type_id, quantity_kg, confirmed_at |
Verifying the signature
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.
Node.js (Express)
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);
});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 "", 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 receiving URL must be
https:// to a public server. Nextfarm rejects private addresses (localhost, 10.x, 192.168.x, 169.254.x…) both when saving and when sending.Full security rules for partners: Security →