> ## Documentation Index
> Fetch the complete documentation index at: https://docs.catenatelematics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Quick Start

> Receive real-time fleet event notifications in under 15 minutes

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.
</Check>

<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).
</Info>

***

## How It Works

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

```mermaid theme={null}
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 theme={null}
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.
</Tip>

***

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

```bash cURL theme={null}
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"
  }'
```

```json Response theme={null}
{
  "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 `*****`.
</Warning>

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.
</Info>

***

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

```json vehicle_location.added payload theme={null}
{
  "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 theme={null}
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:

<AccordionGroup>
  <Accordion title="Filter by Fleet ID" icon="truck">
    Receive events only for specific fleets using Catena's internal fleet IDs.

    ```json theme={null}
        {
          "filters": {
            "fleet_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
          }
        }
    ```
  </Accordion>

  <Accordion title="Filter by Fleet Reference" icon="hashtag">
    Target events for fleets using your custom fleet references (the ID of the fleet in your system).

    ```json theme={null}
        {
          "filters": {
            "fleet_refs": ["ACME-FLEET-001", "ACME-FLEET-002"]
          }
        }
    ```
  </Accordion>

  <Accordion title="Combine Both Filters" icon="filter">
    Use both fleet IDs and fleet references together. Filters use OR logic — events matching either criteria will be delivered.

    ```json theme={null}
        {
          "filters": {
            "fleet_ids": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
            "fleet_refs": ["ACME-FLEET-001"]
          }
        }
    ```
  </Accordion>
</AccordionGroup>

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

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

<Steps>
  <Step title="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 theme={null}
        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.
    </Tip>
  </Step>

  <Step title="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.
  </Step>

  <Step title="Map to Your System">
    If you didn't set a `fleet_ref` when creating the invitation, use the [Organizations API](/api-reference/organizations-api) to map `fleet_id` to your internal customer identifiers.
  </Step>
</Steps>

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

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

***

## What's Next

<CardGroup cols={2}>
  <Card title="Common Questions" icon="circle-question" href="/api-reference/webhooks/webhook-faq">
    Answers to the most frequently asked questions about webhooks, event behavior, and troubleshooting.
  </Card>

  <Card title="Security Reference" icon="shield-check" href="/api-reference/webhooks/webhook-validation">
    Full header reference, key rotation, and security best practices.
  </Card>

  <Card title="Monitoring & Replay" icon="chart-line" href="/api-reference/webhooks/operations-monitoring">
    Track delivery metrics, view logs, replay failed events, and manage the webhook lifecycle.
  </Card>

  <Card title="Notifications API Reference" icon="code" href="/api-reference/notifications-api">
    Complete API reference for all webhook endpoints.
  </Card>
</CardGroup>

***

<Card icon="life-ring" title="Contact Support" href="mailto:support@catenaclearing.io">
  Have questions or need assistance? Our team is here to help you succeed.
</Card>
