Webhooks

Receive your users' vehicle data at your HTTPS endpoints, signed, as it happens.

Draft

App webhooks push vehicle telemetry (location, speed, odometer, fuel, ignition, locks, …) to HTTPS endpoints you register in the app Webhooks tab. Several endpoints per app are allowed (for example Production, Staging and Development); each one has its own URL, data selection and signing secret.

Activation by consent

There is no per-user activation call. A connection forwards its vehicle data to all your endpoints when:

  • the user granted webhooks:receive at consent, together with location:read and offline_access (your app must request them). The consent screen tells the user your app will receive their vehicle data by webhook;
  • your app has at least one endpoint;
  • the connection is active and can still refresh its tokens;
  • the user (or, for organization vehicles, the organization owner) has not removed your webhook in FleetBold Webhooks & API. A removal is sticky: only the user can allow it again;
  • you have not paused the connection (see below).

Tip: when your app needs webhook data from every user, mark webhooks:receive, location:read and offline_access as Required in OAuth & permissions. Users then approve all three or deny the connection.

Only the vehicles the connection can read through /v1 are forwarded. Read the state of a connection with GET /v1/connection/webhook.

Connection webhook state (user token)bash
# Status of this connection's forwarding
curl https://partners-api.fleetbold.com/v1/connection/webhook \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Stop receiving this user's data (or "active" to resume)
curl -X PATCH https://partners-api.fleetbold.com/v1/connection/webhook \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

Endpoints: disabled until verified

  1. Add endpoint: name, stage, public HTTPS URL and the data groups to receive. The signing secret is shown once. The endpoint starts disabled: its configuration is prepared for every eligible connection, but nothing is sent.
  2. Test: FleetBold sends a signed test event ("test": true). A 2xx answer verifies the URL.
  3. Enable: the switch is available once the URL is verified. FleetBold runs a live test first; if it does not answer 2xx, the endpoint stays disabled and you see the result.
  • Changing the URL disables the endpoint and clears its verification: test it and enable it again.
  • Rotating the secret keeps the endpoint enabled. Deliveries switch to the new secret within 5 minutes: accept both secrets meanwhile, then run Test.
  • After repeated failures an endpoint is auto-disabled; fix the receiver and turn it back on (another live test). The other endpoints are not affected.
  • FleetBold support can pause or disable an endpoint; you cannot enable it until the hold is lifted. Nothing is sent while an app is suspended.
  • Events that happen while an endpoint is disabled are not queued. Delivery is at most once: 3 attempts (408, 425, 429 and 5xx are retried), no redirects, answer within 5 s (fixed timeout).

Payload

One vehicle.telemetry event per request, JSON, api_version v1. data holds only the groups the endpoint selected and that a context allows; data_groups lists the groups present in that body. A group with no values is absent; a missing field inside a present group is null. contexts[] lists the connections (and organizations, null for personal vehicles) through which your app reaches the vehicle. vin is set only when one of those connections granted vehicles:read.

  • location (Location, location:read): latitude_deg, longitude_deg, gps_fix, recorded_at.
  • motion (Speed and heading, location:read): speed_mph, speed_kph, speed_recorded_at, heading_deg, heading_recorded_at.
  • mileage (Odometer, vehicle_state:read): odometer_mi, odometer_km, recorded_at.
  • energy (Fuel and battery, vehicle_state:read): fuel_level_pct, battery_level_pct, recorded_at.
  • ignition (Ignition, vehicle_state:read): ignition_on, recorded_at.
  • locks (Door locks, vehicle_state:read): locked, recorded_at.
  • environment (Environment, vehicle_state:read): outside_temp_c, signal_strength (raw device value, not normalised), recorded_at.
  • Coming soon: trips, bookings, notifications. GET /v1/events returns the live catalog. Treat unknown groups as additive.
Example bodyjson
{
  "id": "evt_5f0c2a9b8d7e4f10a1b2c3d4",
  "type": "vehicle.telemetry",
  "api_version": "v1",
  "created_at": "2026-09-28T12:00:03Z",
  "test": false,
  "app_id": "app_01J…",
  "webhook_id": "whk_01J…",
  "data_groups": ["location", "motion", "energy"],
  "contexts": [
    { "connection_id": "conn_01J…", "organization_id": "org_01J…" },
    { "connection_id": "conn_01K…", "organization_id": null }
  ],
  "data": {
    "vehicle": { "id": "veh_01J…", "vin": "1FT7W2BT8HEB21938" },
    "occurred_at": "2026-09-28T12:00:01Z",
    "location": {
      "latitude_deg": 25.667,
      "longitude_deg": -80.398,
      "gps_fix": true,
      "recorded_at": "2026-09-28T12:00:01Z"
    },
    "motion": {
      "speed_mph": 26,
      "speed_kph": 42,
      "speed_recorded_at": "2026-09-28T12:00:01Z",
      "heading_deg": 270.0,
      "heading_recorded_at": "2026-09-28T12:00:01Z"
    },
    "energy": {
      "fuel_level_pct": 62.0,
      "battery_level_pct": null,
      "recorded_at": "2026-09-28T12:00:01Z"
    }
  }
}

Signature verification

  • X-FleetBold-Signature: v1=<hex>: HMAC-SHA256 of "<timestamp>.<raw body>" with the endpoint signing secret.
  • X-FleetBold-Timestamp: Unix seconds when the request was signed.
  • X-FleetBold-Event-Id: The event id (evt_…, equals id in the body). Dedupe on it per endpoint.
  • X-FleetBold-Stream-Id: The endpoint id (whk_…) that received the event.
  • Compute the HMAC over the raw body exactly as received, with that endpoint's secret, and compare in constant time.
  • Reject timestamps older than 5 minutes.
  • Dedupe by event id per endpoint: the same event reaches each of your endpoints once.
  • Answer test events ("test": true) with 2xx and otherwise ignore them.
Node.jsjavascript
import crypto from 'node:crypto';

// rawBody: the exact request body string. Do not re-serialize the parsed JSON.
export function verifyFleetBold(rawBody, headers, secrets) {
  const ts = headers['x-fleetbold-timestamp'];
  const signature = headers['x-fleetbold-signature'] ?? '';
  // Reject requests signed more than 5 minutes ago (replays).
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  // secrets: this endpoint's current secret (+ the previous one for 5 minutes after a rotation).
  return secrets.some((secret) => {
    const expected =
      'v1=' +
      crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}
Pythonpython
import hashlib
import hmac
import time


def verify_fleetbold(raw_body: bytes, headers, secrets) -> bool:
    """raw_body: the exact request bytes. secrets: the endpoint's current secret
    (+ the previous one for 5 minutes after a rotation)."""
    ts = headers.get('X-FleetBold-Timestamp', '')
    signature = headers.get('X-FleetBold-Signature', '')
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    message = ts.encode() + b'.' + raw_body
    for secret in secrets:
        expected = 'v1=' + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
        if hmac.compare_digest(signature, expected):
            return True
    return False

Pause one connection

To stop receiving one user's data without them revoking, pause the connection: with the user's token (PATCH /v1/connection/webhook), from your backend with an app token (client_credentials, scope app:webhooks, confidential clients only) or with the toggle in the console. {"status": "active"} resumes it; a resume can answer limit_reached. A user removal wins: pausing or resuming it answers 409 webhook_removed_by_user. Paused connections do not count toward the webhook connections limit.

Server to serverbash
# 1. App token (confidential clients only)
curl https://partners-api.fleetbold.com/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope=app:webhooks

# 2. Pause (or resume) one connection by its id
curl -X PATCH https://partners-api.fleetbold.com/v1/app/connections/conn_01J…/webhook \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

Limits

  • Webhook endpoints: endpoints per app (3 by default).
  • Webhook connections: connections whose data is forwarded while your app has at least one enabled endpoint (2 for development apps). A new connection over the limit gets limit_reached and nothing is sent for it.
  • Both are shown in the app Overview and the Webhooks tab; FleetBold can raise them.

Go live

To go live with webhooks:receive, FleetBold needs proof that your webhook works: one of your endpoints answered with a 2xx in the last 90 days, either to a Test sent from the Webhooks tab (a signed event with "test": true) or to a real delivery through your own or a test user's connection. Then run the Go live pre-flight again; it is the only scope whose evidence is required.