NodleIoTOpen dashboard →
Documentation

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

  1. Open a fleet and go to its Webhooks page (Fleets › your fleet › Webhooks), or click Set up on the fleet's Webhooks card.
  2. Enter your Webhook URL (must start with http:// or https://) and click Add webhook.
  3. 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.test event 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's data carries; identifier is 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 discarded summary. 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 as sdata:fca6/fc3d whose detections appear as Sightings) have no per-device identity: their events carry the fleet identifier here, the same value as fleet, and any per-tag data is in payload. They only produce ping.detected, never tracked.registered.
  • gps — the scanner's fix at detection time, or null when the detection carried none. accuracy is the horizontal accuracy radius in meters.
  • time — the BLE scan unix timestamp.
  • rssi — received signal strength in dBm measured by the scanning smartphone, or null when not reported.
  • payload — the raw BLE advertisement as the scanner reported it, or null when the detection carried none: a list of field / value pairs, where field is the AD type as a hex byte (e.g. ff, manufacturer data) or a name the pipeline already extracted (e.g. ib_maj), and value is 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.