Webhooks
Forward your fleet's detections to your own server in real time. Each fleet can have a single webhook endpoint. The dashboard is the control plane where you configure it; delivery is handled by the Nodle backend, which POSTs a signed batch of events to your URL after every ingestion cycle.
Setting up a webhook
- Open a fleet and go to its Webhooks page (Fleets › your fleet › Webhooks), or click Set up on the fleet's Webhooks card.
- Enter your Webhook URL (must start with
http://orhttps://) and click Add webhook. - Copy the generated signing secret shown in the reveal banner. It is displayed only once — store it securely to verify incoming requests.
Only admins and editors can manage webhooks; members with the viewer role see a read-only view.
Managing a webhook
- Enable / Disable — pause or resume deliveries without losing the configuration.
- Edit — change the destination URL at any time.
- Rotate secret — generate a new signing secret (shown once). Old signatures stop validating immediately, so update your server at the same time.
- Send test event — deliver a sample
webhook.testevent to confirm your endpoint is reachable. The result (HTTP status or error) is shown inline. - Delete — remove the webhook and stop generating its delivery history. This cannot be undone.
Delivery & reliability
- Batched per cycle — detections are grouped into one request per ingestion cycle, so a single delivery can contain multiple events.
- Retries — a failed delivery is retried up to 5 times before it is discarded (dead-lettered).
- Auto-deactivation — after 3 consecutive discarded deliveries the webhook is turned off and a banner explains why. It retries automatically after ~30 minutes, so a temporarily-down server heals on its own; you can also fix your endpoint and press Enable to retry sooner. Deactivating a webhook yourself is never overridden.
- Payload fields — you choose which optional fields (
gps,time,rssi,payload) each event'sdatacarries;identifieris always included. New webhooks send everything by default. - Delivery log — the table shows Status, Code, Attempts, Error and Date, with a
24h: N delivered · N failed · N discardedsummary. History is retained for 14 days.
Delivery statuses
- Delivered — your endpoint responded with a 2xx status.
- Failed — the delivery did not succeed but is still eligible for retries.
- Discarded — all retries were exhausted; the batch was dropped.
Endpoints should return a 2xx quickly and process the payload asynchronously. Non-2xx responses count as failures.
Payload format
Requests are sent as POST with Content-Type: application/json. The body contains an events array; each event carries an event_id, a type, the fleet identifier and a data object. Filter on type: a batch can mix event types, and the webhook.test event is not a real detection.
{
"events": [
{
"event_id": "3f1c9b2e-...-a7d4",
"type": "ping.detected",
"occurred_at": "2026-07-21T10:15:00Z",
"fleet": "my-fleet-identifier",
"data": {
"identifier": "my-fleet-identifier#1#2",
"gps": {
"latitude": 12.34,
"longitude": 56.78,
"altitude": 9.0,
"geohash": "abc",
"accuracy": 8.6
},
"time": 1663632520,
"rssi": -78.0,
"payload": [
{ "field": "09", "value": "4e42544147" },
{ "field": "ff", "value": "0505005fcd09671b0000dd250010943fff" }
]
}
}
]
}- identifier — the detected device's full identifier (e.g.
ibeacon:<uuid>#<major>#<minor>). Service-data fleets (sdata:…, including carrier-wrapped fleets such assdata:fca6/fc3dwhose detections appear as Sightings) have no per-device identity: their events carry the fleet identifier here, the same value asfleet, and any per-tag data is inpayload. They only produceping.detected, nevertracked.registered. - gps — the scanner's fix at detection time, or
nullwhen the detection carried none.accuracyis the horizontal accuracy radius in meters. - time — the BLE scan unix timestamp.
- rssi — received signal strength in dBm measured by the scanning smartphone, or
nullwhen not reported. - payload — the raw BLE advertisement as the scanner reported it, or
nullwhen the detection carried none: a list offield/valuepairs, wherefieldis the AD type as a hex byte (e.g.ff, manufacturer data) or a name the pipeline already extracted (e.g.ib_maj), andvalueis hex. It is sent as received; decode vendor data on your side.
tracked.registered — one event per device newly registered in the fleet during an ingestion cycle, i.e. the first time it is detected there. It is delivered in the same batch as that cycle's ping.detected events, so a single request can carry both types for the same device. data.identifier is the new device's full identifier; the other data fields come from that device's detection in the same cycle, or are null when there is none. Always filter on type rather than treating every event as a detection.
Verifying the signature
Every request includes an X-Nodle-Signature header of the form sha256=<hex>, where the value is the lowercase-hex HMAC-SHA256 of the raw request body keyed with your signing secret. Recompute it on your server and compare using a constant-time equality check before trusting the payload.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(header),
Buffer.from(expected),
);
}Verify over the exact bytes you received — do not re-serialize the JSON first, or the signature will not match.