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
- Sign in to your fleet dashboard on KATSANA.
- Click on Fleet Settings in the navigation menu.
- Select the Webhooks tab.
- 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 other2xxcodes (such as201or204),3xxredirects, 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
- Extract timestamp and signature: Split the
X-Signatureheader value on,. Split each parameter on=to obtain the integer timestamptand signature stringv1. - Construct the signed payload: Concatenate the timestamp string, a literal period
., and the exact raw request body verbatim:${timestamp}.${rawRequestBody} - 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.
- Compare signatures securely: Compare the received
v1signature with the computed signature using a constant-time string comparison function to protect against timing attacks. - 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';