> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://catena.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://catena.ferndocs.com/_mcp/server.

# Webhook Quick Start

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.

> **Check**
>
> **What You'll Build:** A verified webhook subscription that receives live `vehicle_location.added` events and handles them securely.

> **Info**
>
> **Prerequisites:** You'll need a valid access token before starting. If you haven't authenticated yet, see [Step 1 of the Quickstart Guide](/get-started/quickstart).

---

## How It Works

Instead of [polling the API](/api-reference/telematics/telematics-api) for updates, Catena pushes a notification to your server the moment data changes.

```mermaid
sequenceDiagram
    participant TSP as TSP (e.g. Samsara)
    participant Catena as Catena Platform
    participant You as Your Server

    TSP->>Catena: New vehicle location data
    Catena->>Catena: Normalize & store
    Catena->>You: POST /your-webhook-endpoint
    Note over You: Decompress gzip body
    Note over You: Verify HMAC signature
    You-->>Catena: 202 Accepted
    You->>You: Process event async
```

---

## 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`**

```python 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
```

> **Tip**
>
> **Not ready to build yet?** Use [webhook.site](https://webhook.site) or [RequestBin](https://requestbin.com) 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](/get-started/platform-concepts).

**`cURL`**

```bash 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`**

```json 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"
}
```

> **Warning**
>
> **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.

> **Info**
>
> **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`**

```json 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](/get-started/quickstart#step-2-create-a-fleet-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](/freight-concepts/elds) 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`**

```python 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:

#### Filter by Fleet ID

Receive events only for specific fleets using Catena's internal fleet IDs.

```json
    {
      "filters": {
        "fleet_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
      }
    }
```

#### Filter by Fleet Reference

Target events for fleets using your custom fleet references (the ID of the fleet in your system).

```json
    {
      "filters": {
        "fleet_refs": ["ACME-FLEET-001", "ACME-FLEET-002"]
      }
    }
```

#### Combine Both Filters

Use both fleet IDs and fleet references together. Filters use OR logic — events matching either criteria will be delivered.

```json
    {
      "filters": {
        "fleet_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
        "fleet_refs": ["ACME-FLEET-001"]
      }
    }
```

> **Tip**
>
> **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.

### Recommended: Single Webhook per Event Type

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.

#### 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.

```bash
    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"}'
```

> **Tip**
>
> Use wildcard patterns like `vehicle.*` to subscribe to all actions for a resource (added, modified, removed) with a single subscription.

#### 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.

#### Map to Your System

If you didn't set a `fleet_ref` when creating the invitation, use the [Organizations API](/api-reference/orgs/organizations-api) to map `fleet_id` to your internal customer identifiers.

> **Info**
>
> **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](/recipes/create-invitations) 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.

> **Info**
>
> **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](/recipes/create-invitations) 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](/api-reference/notifications/webhooks/webhook-faq)

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

#### [Security Reference](/api-reference/notifications/webhooks/webhook-validation)

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

#### [Monitoring & Replay](/api-reference/notifications/webhooks/operations-monitoring)

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

#### [Notifications API Reference](/api-reference/notifications/notifications-api)

Complete API reference for all webhook endpoints.

---

#### [Contact Support](mailto:support@catenaclearing.io)

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