Event notifications

Webhook integration

Receive real-time event notifications for vehicle immobilization, mobilization, RFID driver updates, and geofence checkpoints.

Overview

KATSANA Fleet Webhooks allow your backend systems to receive real-time HTTP POST notifications whenever critical fleet events occur. Rather than continuously polling the REST API, your server receives immediate notifications when vehicles are immobilized, mobilized, driver RFID tags are updated, or geofence checkpoints are crossed.


1. Creating a webhook

Webhooks are configured through the KATSANA Fleet web interface.

Step-by-step instructions

  1. Sign in to your fleet dashboard on KATSANA.
  2. Click on Fleet Settings in the navigation menu.
  3. Select the Webhooks tab.
  4. Click the Add Webhook button.

Configuration fields

Field Description
URL The public HTTPS endpoint of your webhook handler (e.g., https://api.example.com/webhooks/katsana).
Secret The secret signing string used to generate HMAC-SHA256 signatures for request verification. By default, KATSANA generates a cryptographically random secret, or you can input your own preferred value. Save a copy securely before submitting, as it will not be displayed again.
Event The event type this webhook will receive. Each webhook handles a single event type. If you want a single endpoint to handle multiple event types, create multiple webhooks pointing to the same URL.
Sample Request A live preview of the payload parameters delivered for the selected event type.

2. Webhook events

KATSANA supports four event notifications. Each payload is delivered as a JSON object containing the relevant telematics identifiers and event data.

2.1. Device immobilized

Triggered when an on-board telematics unit equipped with relay immobilization has been successfully immobilized.

  • Event ID: Device immobilized
Field Type Required Description
device_id Integer Yes The internal device identifier.
event String Yes Event identifier string (Device immobilized).
{
  "device_id": 123,
  "event": "Device immobilized"
}

2.2. Device mobilized

Triggered when an immobilized vehicle’s ignition cutoff is released and the vehicle is successfully mobilized.

  • Event ID: Device mobilized
Field Type Required Description
device_id Integer Yes The internal device identifier.
event String Yes Event identifier string (Device mobilized).
{
  "device_id": 123,
  "event": "Device mobilized"
}

2.3. Authorized RFID updated

Triggered when an on-board RFID / iButton driver identification unit has its authorized driver card whitelist updated.

  • Event ID: Authorized RFID updated
Field Type Required Description
device_id Integer Yes The internal device identifier.
event String Yes Event identifier string (Authorized RFID updated).
rfids Array<String> Yes Updated list of authorized RFID keys (hexadecimal iButton ID format).
{
  "device_id": 123,
  "event": "Authorized RFID updated",
  "rfids": [
    "01000014B2C3D4",
    "01000014B2C3D5"
  ]
}

2.4. Checkpoint triggered

Triggered when a vehicle enters, exits, or crosses a configured geofence checkpoint boundary.

  • Event ID: Checkpoint triggered
Field Type Required Description
device_id Integer Yes The internal device identifier.
event String Yes Event identifier string (Checkpoint triggered).
policy_id Integer Yes The checkpoint geofence policy ID.
message String Yes Push notification message text sent to users.
checkpoint_name String Yes Name assigned to the checkpoint (e.g., HQ Depot).
checkpoint_type String Yes Type of boundary event: enter, exit, or cross.
ping.latitude Float Yes Vehicle latitude at the time the checkpoint triggered.
ping.longitude Float Yes Vehicle longitude at the time the checkpoint triggered.
ping.speed Float Yes Vehicle speed (km/h) when the checkpoint triggered.
{
  "device_id": 123,
  "event": "Checkpoint triggered",
  "policy_id": 456,
  "message": "Vehicle BAA1234 has entered HQ Depot",
  "checkpoint_name": "HQ Depot",
  "checkpoint_type": "enter",
  "ping": {
    "latitude": 3.139003,
    "longitude": 101.686855,
    "speed": 38.5
  }
}

3. Receiving your webhook

3.1. Webhook request format

When an event triggers, KATSANA sends an HTTP POST request to your configured endpoint with header Content-Type: application/json and an X-Signature header:

POST /webhook HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-Signature: t=1521012526, v1=1dc18446666a9d3c8208a613c2fc7474d4d67fa07cb9179ddf92fc221d37a50f

{
  "device_id": 123,
  "event": "Device immobilized"
}

3.2. Responding to a webhook

Your webhook receiver endpoint must return an HTTP 200 status code.

  • All response status codes other than 200—including other 2xx codes (such as 201 or 204), 3xx redirects, or error codes—are treated as delivery failures.
  • Retry behavior: If your endpoint does not respond with 200, KATSANA automatically retries sending the webhook every 30 seconds, up to 5 times.
  • You can inspect the last delivery attempt timestamp and delivery status in the Last Delivered At and Last Delivered Status columns within Fleet Settings.

3.3. HTTPS requirement

KATSANA validates SSL/TLS connectivity before transmitting webhook payloads. Your webhook endpoint must be hosted on an HTTPS server with a valid SSL certificate. Insecure plain HTTP endpoints are not supported.

3.4. Handling CSRF protection

Modern web frameworks (such as Laravel, Ruby on Rails, Django, and Express csurf) often reject POST requests lacking CSRF tokens. Because webhooks originate from external servers, configure your application router to exempt your webhook URL route from CSRF middleware verification.


4. Verifying webhook signatures

To ensure that incoming webhook requests originate from KATSANA and have not been altered in transit, verify the signature contained in the X-Signature HTTP header.

4.1. The signature header

The X-Signature header includes a UNIX timestamp (t=) and a hexadecimal SHA256 HMAC signature (v1=):

X-Signature: t=1520394606, v1=d4d037868dc3e8078468d67fb3b8bd8a5ee0d01134ec97e87ae777873ca6367a

4.2. Verification algorithm

  1. Extract timestamp and signature: Split the X-Signature header value on ,. Split each parameter on = to obtain the integer timestamp t and signature string v1.
  2. Construct the signed payload: Concatenate the timestamp string, a literal period ., and the exact raw request body verbatim:
    ${timestamp}.${rawRequestBody}
  3. Compute the expected signature: Compute an HMAC using the SHA256 hash algorithm, passing your configured webhook secret as the key and the combined string as the message.
  4. Compare signatures securely: Compare the received v1 signature with the computed signature using a constant-time string comparison function to protect against timing attacks.
  5. Enforce timestamp tolerance: Calculate the difference between the server’s current time and the received timestamp (Math.abs(currentTime - timestamp)). Reject requests older than your tolerance threshold (e.g., 300 seconds / 5 minutes) to protect against replay attacks.

5. Verification code samples

Node.js (Express)

import express from 'express';
import crypto from 'crypto';

const app = express();
const WEBHOOK_SECRET = process.env.KATSANA_WEBHOOK_SECRET || 'your-webhook-secret';
const TOLERANCE_SECONDS = 300; // 5 minutes

// Capture raw body buffer for accurate signature validation
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signatureHeader = req.headers['x-signature'];
  if (!signatureHeader) {
    return res.status(400).send('Missing X-Signature header');
  }

  // 1. Parse header elements
  const elements = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.trim().split('='))
  );
  const timestamp = parseInt(elements.t, 10);
  const receivedSignature = elements.v1;

  if (isNaN(timestamp) || !receivedSignature) {
    return res.status(400).send('Malformed X-Signature header');
  }

  // 2. Prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) {
    return res.status(403).send('Webhook timestamp outside tolerance window');
  }

  // 3. Construct payload and compute HMAC
  const rawBody = req.body.toString('utf8');
  const payload = `${timestamp}.${rawBody}`;
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload, 'utf8')
    .digest('hex');

  // 4. Constant-time comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(receivedSignature, 'utf8'),
    Buffer.from(expectedSignature, 'utf8')
  );

  if (!isValid) {
    return res.status(400).send('Invalid signature');
  }

  // 5. Process event
  const data = JSON.parse(rawBody);
  console.log(`Received verified event: ${data.event} for device ${data.device_id}`);

  // Return HTTP 200 to acknowledge receipt
  return res.status(200).send('OK');
});

app.listen(3000, () => console.log('Webhook server listening on port 3000'));

Python (Flask)

import hmac
import hashlib
import time
import os
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("KATSANA_WEBHOOK_SECRET", "your-webhook-secret").encode("utf-8")
TOLERANCE_SECONDS = 300  # 5 minutes

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    signature_header = request.headers.get("X-Signature")
    if not signature_header:
        return "Missing signature header", 400

    # 1. Parse timestamp and signature
    parts = dict(item.strip().split("=", 1) for item in signature_header.split(","))
    timestamp_str = parts.get("t")
    received_signature = parts.get("v1")

    if not timestamp_str or not received_signature:
        return "Invalid signature format", 400

    try:
        timestamp = int(timestamp_str)
    except ValueError:
        return "Invalid timestamp", 400

    # 2. Check tolerance window
    now = int(time.time())
    if abs(now - timestamp) > TOLERANCE_SECONDS:
        return "Webhook timestamp outside tolerance window", 403

    # 3. Construct payload and compute HMAC
    raw_body = request.get_data()
    payload = f"{timestamp}.".encode("utf-8") + raw_body
    expected_signature = hmac.new(WEBHOOK_SECRET, payload, hashlib.sha256).hexdigest()

    # 4. Constant-time comparison
    if not hmac.compare_digest(received_signature, expected_signature):
        return "Invalid signature", 400

    # 5. Process event
    data = request.get_json()
    print(f"Verified event: {data.get('event')} on device {data.get('device_id')}")

    # Return HTTP 200
    return "OK", 200

if __name__ == "__main__":
    app.run(port=3000)

PHP

<?php
// Extract timestamp and signature from the X-Signature header
$signatureHeader = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (empty($signatureHeader)) {
    http_response_code(400);
    exit('Missing X-Signature header');
}

$parts = [];
foreach (explode(',', $signatureHeader) as $pair) {
    [$key, $val] = explode('=', trim($pair), 2);
    $parts[$key] = $val;
}

$timestamp = isset($parts['t']) ? (int) $parts['t'] : 0;
$signature = $parts['v1'] ?? '';

// Construct verification payload
$rawBody = file_get_contents('php://input');
$payload = $timestamp . '.' . $rawBody;

// Generate expected HMAC-SHA256 signature
$secret = getenv('KATSANA_WEBHOOK_SECRET') ?: '<webhook secret>';
$expected = hash_hmac('sha256', $payload, $secret);

// Compare signatures using constant-time comparison
if (!hash_equals($signature, $expected)) {
    http_response_code(400);
    exit('Invalid signature');
}

// Verify timestamp tolerance (e.g., 300 seconds)
$threshold = 300;
if (abs(time() - $timestamp) > $threshold) {
    http_response_code(403);
    exit('Timestamp outside tolerance');
}

// Process the verified event
$data = json_decode($rawBody, true);
error_log("Received verified event: " . $data['event']);

// Indicate successful reception with HTTP 200
http_response_code(200);
echo 'OK';