Skip to main content
This guide gets you from zero to a working, verified webhook in four steps. You’ll create an endpoint, subscribe to an event, understand what you’ll receive, and verify it is authentic.
What You’ll Build: A verified webhook subscription that receives live vehicle_location.added events and handles them securely.
Prerequisites: You’ll need a valid access token before starting. If you haven’t authenticated yet, see Step 1 of the Quickstart Guide.

How It Works

Instead of polling the API for updates, Catena pushes a notification to your server the moment data changes.

Step 1: Create Your Endpoint

Set up an HTTPS URL on your server that accepts POST requests and returns 202 Accepted within 3 seconds. Return the acknowledgment immediately — process the event in a background job.
Python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/catena", methods=["POST"])
def handle_webhook():
    # Acknowledge receipt immediately — process asynchronously
    return jsonify({"status": "received"}), 202
Not ready to build yet? Use webhook.site or RequestBin to get a public HTTPS URL instantly and inspect live payloads before writing any code.

Step 2: Subscribe to an Event

Call the Notifications API to create a subscription. The example below subscribes to vehicle_location.added, which fires every time a new GPS location is ingested for any vehicle in your connected fleets.
cURL
curl -X POST \
  --url https://api.catenatelematics.com/v2/notifications/webhooks \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://your-domain.com/webhooks/catena",
    "event_name": "vehicle_location.added"
  }'
Response
{
  "id": "247b2dea-a030-48b7-9a05-ee33c1b6ab0a",
  "url": "https://your-domain.com/webhooks/*****",
  "event_name": "vehicle_location.added",
  "filters": null,
  "secret": "wX9kLmP2nQrT4vYz",
  "status": "active",
  "created_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:30:00Z"
}
Save your secret immediately. The secret is shown in full only once. Copy it to your secrets manager now — all subsequent reads return *****.
Catena will begin delivering events to your endpoint right away. The id in the response is your subscription ID — you’ll use it to manage and monitor this webhook later.
Unique Subscriptions: You can only create one subscription per unique combination of event type, endpoint URL, and filters. To send the same events to multiple endpoints, create separate subscriptions with different URLs.

Step 3: Understand What You’ll Receive

Every webhook is a POST request with a gzip-compressed JSON body. After decompressing, the payload follows this structure:
vehicle_location.added payload
{
  "id": "01944b2c-7f3a-7e1b-8c2d-9e5f0a1b2c3d",
  "event_name": "vehicle_location.added",
  "version": "1.2",
  "webhook_id": "247b2dea-a030-48b7-9a05-ee33c1b6ab0a",
  "timestamp": "2026-01-15T10:30:00Z",
  "delivery_attempt": 1,
  "data": [
    {
      "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
      "fleet_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "fleet_ref": "ACME-FLEET-001",
      "source_name": "samsara",
      "occurred_at": "2026-01-15T10:29:55Z",
      "vehicle_id": "a3bb189e-8bf9-3888-9912-ace4e6543002",
      "location": {
        "type": "Point",
        "coordinates": [-104.9876, 39.7392]
      },
      "speed": 27.8,
      "odometer": 154823.4,
      "fuel_level": 68.5
    }
  ]
}
A few fields worth calling out:
  • data — Always an array. Iterate over it even when you expect a single record.
  • fleet_ref — Your own identifier for the fleet, set when you created the invitation. Use this to route events to the right customer in your system without a separate API lookup. Will be null if no fleet_ref was set at invitation time.
  • source_name — The underlying telematics provider that reported the data (e.g. samsara, motive, geotab).
  • occurred_at — When the event happened at the TSP. Use this for ordering, not the outer timestamp.
  • delivery_attempt — Increments with each retry. If this is greater than 1, the event may be a duplicate.

Step 4: Verify the Signature

Before processing any event, verify it genuinely came from Catena. Every request includes two security headers: X-Catena-Signature (an HMAC-SHA256 digest) and X-Catena-Timestamp. The signature is computed as:
Base64(HMAC-SHA256(secret, timestamp + "." + uncompressed_json_body))
Python
import base64
import gzip
import hashlib
import hmac
from datetime import datetime, timedelta, timezone

from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "wX9kLmP2nQrT4vYz"  # Load from your secrets manager


def verify_catena_webhook(headers: dict, raw_body: bytes, secret: str) -> bool:
    timestamp = headers.get("X-Catena-Timestamp")
    signature = headers.get("X-Catena-Signature")

    if not timestamp or not signature:
        return False

    # Reject requests older than 5 minutes to prevent replay attacks
    try:
        ts = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
        if datetime.now(timezone.utc) - ts > timedelta(minutes=5):
            return False
    except ValueError:
        return False

    # Decompress — Catena always sends gzip. Verify against the uncompressed string.
    try:
        payload = gzip.decompress(raw_body).decode("utf-8")
    except Exception:
        return False

    # Recompute and compare using constant-time comparison to prevent timing attacks
    signed_payload = f"{timestamp}.{payload}".encode()
    expected = base64.b64encode(
        hmac.new(secret.encode(), signed_payload, hashlib.sha256).digest()
    ).decode()

    return hmac.compare_digest(signature, expected)


@app.route("/webhooks/catena", methods=["POST"])
def handle_webhook():
    raw_body = request.get_data()

    if not verify_catena_webhook(request.headers, raw_body, WEBHOOK_SECRET):
        return jsonify({"error": "Invalid signature"}), 401

    event = request.get_json()

    # Enqueue for background processing, then acknowledge immediately
    # e.g. enqueue(event["event_name"], event["data"])

    return jsonify({"status": "received"}), 202
You’re live. Catena is now delivering real-time fleet events to your endpoint.

Event Filtering

Use filters to scope webhook delivery to specific fleets, reducing unnecessary notifications and processing overhead:
Receive events only for specific fleets using Catena’s internal fleet IDs.
    {
      "filters": {
        "fleet_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
      }
    }
Target events for fleets using your custom fleet references (the ID of the fleet in your system).
    {
      "filters": {
        "fleet_refs": ["ACME-FLEET-001", "ACME-FLEET-002"]
      }
    }
Use both fleet IDs and fleet references together. Filters use OR logic — events matching either criteria will be delivered.
    {
      "filters": {
        "fleet_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
        "fleet_refs": ["ACME-FLEET-001"]
      }
    }
Omit Filters to Receive All Events: If you don’t specify any filters, you’ll receive all events of the subscribed type across your entire organization.
The recommended approach is to create one webhook subscription per event type without any filters. This gives you a single, organization-wide stream that automatically covers all fleets — including new ones you onboard in the future.
1

Create One Subscription per Event Type

Subscribe to an event type (e.g., vehicle.modified) without filters to receive events from all fleets across your organization.
    curl -X POST \
      --url https://api.catenatelematics.com/v2/notifications/webhooks \
      -H 'Authorization: Bearer <token>' \
      -H 'Content-Type: application/json' \
      -d '{"url": "https://your-domain.com/webhooks/catena", "event_name": "vehicle.modified"}'
Use wildcard patterns like vehicle.* to subscribe to all actions for a resource (added, modified, removed) with a single subscription.
2

Identify the Fleet

Each event payload includes both a fleet_id and a fleet_ref field. Use fleet_ref — your own identifier for the fleet set at invitation time — to route events to the right customer without a separate API lookup. Note that fleet_ref will be null if it wasn’t set when the invitation was created.
3

Map to Your System

If you didn’t set a fleet_ref when creating the invitation, use the Organizations API to map fleet_id to your internal customer identifiers.
Invitation Lifecycle Events: To track the full sequence of webhook events when onboarding a fleet — from invitation creation through to data ingestion — see the Inviting Fleets guide. If you already follow the recommended approach above for invitation.*, share_agreement.*, connection.*, and fleet_connection.* events, you do not need to set a callback_url on the invitation.
Invitation Lifecycle Events: To track the full sequence of webhook events when onboarding a fleet — from invitation creation through to data ingestion — see the Inviting Fleets guide. If you already follow the recommended approach above for invitation.*, share_agreement.*, connection.*, and fleet_connection.* events, you do not need to set a callback_url on the invitation.

What’s Next

Common Questions

Answers to the most frequently asked questions about webhooks, event behavior, and troubleshooting.

Security Reference

Full header reference, key rotation, and security best practices.

Monitoring & Replay

Track delivery metrics, view logs, replay failed events, and manage the webhook lifecycle.

Notifications API Reference

Complete API reference for all webhook endpoints.

Contact Support

Have questions or need assistance? Our team is here to help you succeed.