Skip to main content
Fleetalyse
Partner API & Tracking API

Webhooks

Event catalogue, signatures, retries, replay and deduplication.

The Fleetalyse Partner Programme is running as a pilot. The API documented here is available to approved partners; you can build against test mode as soon as your partner account is open. About partnering with Fleetalyse

Webhooks tell your server when something finishes — an activation completes, a fleet is ready, an invoice is paid — so you do not have to poll. Each endpoint belongs to one mode and receives only the events it subscribes to, within the fleets it may see.

Register an endpoint

  • Portal: Webhooks → Add endpoint, or POST /webhook-endpoints with url, event_types (or ["*"] for everything you may receive), optional fleet_ids and description.
  • Requirements: https://, port 443 or 8443, a public address (private, loopback, link-local, metadata and reserved addresses are refused, and so are IP literals in unusual notation). Redirects are never followed.
  • The response contains the signing secret whsec_… once.
  • The endpoint stays pending until it answers the signed test event (POST /webhook-endpoints/{id}/test, or the portal button) with a 2xx status. Only then does it receive events.
  • Changing the URL or re-enabling a disabled endpoint returns it to pending.

Event catalogue

TypeWhenCreator needsScope
customer.readyThe first fleet of a customer is ready: the customer can be used for activations.customers.readFleet
customer.provisioning_failedSetting up the first fleet of a customer failed.customers.readFleet
fleet.readyA fleet is provisioned and ready for devices and activations.fleets.readFleet
fleet.provisioning_failedProvisioning a fleet failed; see the operation for details.fleets.readFleet
fleet.plan_changedA fleet-level plan change completed.fleets.readFleet
service.activation_completedA tracking service is confirmed active; the usage charge starts at effective_at.devices.readFleet
service.activation_failedAn activation failed permanently; nothing is charged.devices.readFleet
service.deactivation_completedA service is confirmed deactivated; the usage charge stops at effective_at.devices.readFleet
service.deactivation_failedA deactivation could not be completed; the service is still active and chargeable.devices.readFleet
service.reactivation_completedAn inactive service is active again (a new usage period starts).devices.readFleet
service.plan_changedThe plan of a service changed at effective_at.devices.readFleet
operation.requires_attentionAn operation needs Fleetalyse or partner attention (it will not retry by itself).operations.readFleet
operation.cancelledAn operation was cancelled or superseded by a newer request.operations.readFleet
invoice.finalisedAn invoice was finalised and is due.billing.readPartner
invoice.paidAn invoice was paid.billing.readPartner
invoice.payment_failedAutomatic collection of an invoice failed.billing.readPartner
invoice.payment_action_requiredYour bank needs you to authenticate a payment.billing.readPartner
partner.billing_restrictedNew charge-creating actions are paused until the balance is paid.billing.readPartner
partner.billing_restoredThe billing restriction was lifted.billing.readPartner
hardware.order_dispatchedA hardware order (or part of it) was dispatched.hardware.readPartner
test.pingThe signed test event sent from the portal or POST /webhook-endpoints/{id}/test.—Test only

Fleet events (customer, fleet, service, operation) carry a fleet and reach endpoints for that fleet. Partner events (invoice, partner, hardware) have no fleet and go only to endpoints for all fleets whose creator holds the scope. An endpoint limited to selected fleets can subscribe to fleet events only.

Payload

JSON
{
  "id": "evt_example_001",
  "type": "service.activation_completed",
  "schema_version": "1",
  "created_at": "2026-10-01T09:30:02Z",
  "livemode": true,
  "resource": { "type": "service", "id": "svc_example_01", "version": 3 },
  "data": {
    "customer_id": "cus_example_a",
    "fleet_id": "flt_example_a",
    "device_id": "dev_example_01",
    "service_id": "svc_example_01",
    "operation_id": "op_example_1042",
    "plan_code": "pro",
    "status": "active",
    "billing_status": "accruing",
    "external_order_reference": "ORDER-1042",
    "effective_at": "2026-10-01T09:30:00Z"
  }
}

Headers: Fleetalyse-Signature, Fleetalyse-Event-Id, Fleetalyse-Event-Type, Fleetalyse-Delivery-Attempt, Fleetalyse-Mode and, for replays, Fleetalyse-Replay: true. resource.version increases with every change of the resource.

Verify the signature

The header looks like t=1791890402,v1=5257a869…. Compute HMAC-SHA256 with your signing secret (the whole whsec_… string) over <t>.<raw request body>, compare it in constant time with every v1 value, and reject timestamps more than 300 seconds from your clock. Always use the raw body bytes — re-encoding the JSON breaks the signature.

PHP
<?php
// Verify a Fleetalyse webhook (PHP 8). $secret = your whsec_… signing secret.
function fleetalyse_verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
    $t = null;
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($k === 't' && ctype_digit($v)) {
            $t = (int)$v;
        } elseif ($k === 'v1') {
            $signatures[] = $v;
        }
    }
    if ($t === null || $signatures === [] || abs(time() - $t) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($signatures as $sig) {
        if (hash_equals($expected, $sig)) {
            return true;
        }
    }
    return false;
}

$raw = file_get_contents('php://input');                       // the raw body, unchanged
if (!fleetalyse_verify($raw, $_SERVER['HTTP_FLEETALYSE_SIGNATURE'] ?? '', getenv('FLEETALYSE_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
$event = json_decode($raw, true);
if (already_processed($event['id'])) {                          // your dedupe table
    http_response_code(200);
    exit;
}
queue_for_processing($event);                                   // do the work after answering
http_response_code(204);

Node.js
// Verify a Fleetalyse webhook (Node.js 18+, Express). Keep the RAW body.
const crypto = require('crypto');
const express = require('express');
const app = express();

function fleetalyseVerify(rawBody, header, secret, tolerance = 300) {
  let t = null;
  const sigs = [];
  for (const part of String(header || '').split(',')) {
    const [k, v] = part.trim().split('=', 2);
    if (k === 't' && /^\d+$/.test(v)) t = Number(v);
    else if (k === 'v1' && /^[0-9a-f]{64}$/.test(v)) sigs.push(v);
  }
  const nowSeconds = Math.floor(new Date().getTime() / 1000);
  if (t === null || sigs.length === 0 || Math.abs(nowSeconds - t) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return sigs.some((s) => crypto.timingSafeEqual(Buffer.from(s, 'hex'), Buffer.from(expected, 'hex')));
}

app.post('/webhooks/fleetalyse', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!fleetalyseVerify(raw, req.get('Fleetalyse-Signature'), process.env.FLEETALYSE_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(raw);
  res.sendStatus(204);          // answer first …
  handleOnce(event);            // … then process, skipping event ids you have already seen
});

Python
# Verify a Fleetalyse webhook (Python 3, Flask). Keep the RAW body.
import hashlib, hmac, os, time
from flask import Flask, request

app = Flask(__name__)

def fleetalyse_verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    t, sigs = None, []
    for part in (header or '').split(','):
        k, _, v = part.strip().partition('=')
        if k == 't' and v.isdigit():
            t = int(v)
        elif k == 'v1':
            sigs.append(v)
    if t is None or not sigs or abs(int(time.time()) - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)

@app.post('/webhooks/fleetalyse')
def fleetalyse_webhook():
    raw = request.get_data()   # bytes exactly as sent
    if not fleetalyse_verify(raw, request.headers.get('Fleetalyse-Signature', ''), os.environ['FLEETALYSE_WEBHOOK_SECRET']):
        return '', 400
    event = request.get_json()
    enqueue_once(event)        # dedupe on event['id'], process asynchronously
    return '', 204

Respond, retry, disable

  • Answer with any 2xx within 10 seconds, then do the work. Anything else (including redirects and timeouts) counts as a failure.
  • Failed deliveries are retried with backoff and jitter: 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h (then every 6 h) until the event is 72 hours old.
  • After 3 failures in a row an endpoint gets one delivery at a time, at most every 30 seconds and backing off to every 10 minutes; the others wait and follow as soon as one succeeds. Queued events of other endpoints are never held up by it.
  • Delivery is at-least-once and not ordered: the same event can arrive twice, and a later event can arrive first.
  • An endpoint with no successful delivery for 72 hours is disabled and the account owner is emailed. Operations can still be polled and missed events listed with GET /events.

Deduplicate and order

  • Store the event id of every event you processed (a unique index is enough) and skip ids you have seen. Replays and retries keep the same id.
  • When order matters, compare resource.version with what you stored, or fetch the resource (GET /services/{id}, GET /operations/{id}) and act on its current state.

Rotate the signing secret

POST /webhook-endpoints/{id}/rotate-secret returns a new secret. For overlap_seconds (default 24 hours, up to 7 days) every request is signed with both secrets — two v1 values — so you can deploy the new secret without dropping events. In live mode the portal asks for your two-factor code before revealing or rotating a secret.

Delivery log and replay

  • GET /webhook-endpoints/{id}/deliveries lists attempts: status, attempts, last HTTP status, a redacted excerpt of your response and the next attempt.
  • POST /events/{id}/replay (optionally with endpoint_id) redelivers an event with the same id and body — up to 20 replays per endpoint per hour.
  • GET /events and GET /events/{id} list events your key may see, newest first (filters type, created_after, created_before).
WhatsApp us