Freedam

Webhooks

Webhooks deliver real-time events from Freedam to an HTTPS endpoint you control. Use them in place of polling: when an asset is ingested, a share is opened, or an upload batch finishes, Freedam sends a signed POST with a JSON payload to your URL.

Lifecycle

  1. Register an endpoint with POST /v1/webhooks. The response contains the signing secret, shown once.
  2. Verify every incoming request with the X-Freedam-Signature-256 and X-Freedam-Timestamp headers (see below).
  3. Acknowledge with a 2xx response within 30 seconds. Any other status, a timeout, or a connection error counts as a failed attempt.
  4. Failed attempts are retried up to 5 attempts in total, waiting 10 seconds, 1 minute, 5 minutes, then 30 minutes between attempts.
  5. Repeated failures disable the endpoint. After 10 failed attempts in a row, the endpoint is switched off. Any successful delivery resets the counter.

Endpoints

Webhook endpoints belong to the user who owns the API token. Each token only sees and manages its own endpoints.

Method Path Description
GET /v1/webhooks List your endpoints (paginated, per_page defaults to 20).
POST /v1/webhooks Create an endpoint.
GET /v1/webhooks/{id} Read an endpoint, including failure_count and last_delivery_at.
PUT /v1/webhooks/{id} Update url, description, events, or is_active.
DELETE /v1/webhooks/{id} Permanently delete an endpoint.
POST /v1/webhooks/{id}/test Queue a synthetic webhook.test event to this endpoint only.
GET /v1/webhooks/{id}/deliveries List recent deliveries (paginated, newest first).
POST /v1/webhooks/{id}/rotate-secret Generate a new signing secret.

Creating an endpoint

curl -X POST https://your-freedam-instance.com/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/freedam/webhook",
    "events": ["asset.created", "asset.updated", "share.created"],
    "description": "Production pipeline"
  }'

Request fields:

Field Rules
url Required. A valid URL, up to 2048 characters.
events Required. At least one event name from the table below, or * for all events.
description Optional. Up to 255 characters.

Response (201):

{
    "data": {
        "id": 1,
        "url": "https://example.com/freedam/webhook",
        "description": "Production pipeline",
        "events": ["asset.created", "asset.updated", "share.created"],
        "is_active": true,
        "secret": "the-signing-secret",
        "created_at": "2026-05-06T10:00:00+00:00"
    },
    "message": "Webhook created. Save the secret - it will not be shown again."
}

Store secret server-side. It is never returned again. If you lose it, call POST /v1/webhooks/{id}/rotate-secret.

Event payload shape

Every delivery body has the same envelope. data depends on the event type.

{
    "event": "asset.updated",
    "timestamp": "2026-05-06T10:00:00+00:00",
    "data": {
        "gaid": "IM-20260506-UDXZH8PP6VPW",
        "changed_fields": ["title", "description"]
    }
}

Payloads carry identifiers, not full records: assets are identified by their GAID, as everywhere in the API. Fetch the current state with the REST API (for example GET /v1/assets/{gaid}) when you need more.

Available events

Event Triggered when data fields
asset.created A new asset finishes ingestion and business rules. gaid
asset.updated An asset's metadata changes. gaid, changed_fields
asset.deleted An asset is moved to the trash or permanently deleted. gaid, hard_delete
asset.downloaded A user downloads an asset. gaid, resolution
collection.created A collection is created. collection_id, name
collection.updated A collection changes. collection_id, name, changed_fields
collection.deleted A collection is deleted. collection_id, name
collection.assets_added Assets are added to a collection, manually or by a collection rule. collection_id, asset_gaids
collection.assets_removed Assets are removed from a collection. collection_id, asset_gaids
share.created A share link is created, in Freedam or through the API. share_id, share_token
share.accessed Someone opens a share link or downloads from it. share_id, action (view or download)
upload.batch_completed An upload batch finishes with at least one successful file. batch_id, success_count, failure_count
upload.batch_failed Every file in an upload batch failed. batch_id, reason
workflow.transitioned An asset moves to another workflow status. gaid, from_status, to_status
webhook.test Sent by POST /v1/webhooks/{id}/test to that endpoint only. message

Subscribe to * to receive every event, including events added later. webhook.test is not a subscribable event: POST /v1/webhooks/{id}/test sends it to that endpoint only, whatever its events list.

Verifying signatures

Every delivery includes these headers:

Header Value
X-Freedam-Signature-256 Hex-encoded HMAC-SHA256 of {timestamp}.{raw-body}.
X-Freedam-Timestamp Unix time in seconds when the delivery attempt was signed.
Content-Type application/json

To verify a request:

  1. Read the raw request body before any JSON parsing.
  2. Compute HMAC-SHA256(secret, "{X-Freedam-Timestamp}.{raw-body}") and hex-encode it.
  3. Compare it to X-Freedam-Signature-256 with a constant-time comparison.
  4. Reject the request if the timestamp is more than 5 minutes away from your clock. This blocks replayed requests.

Each retry is signed again with a fresh timestamp, so a retried delivery still passes the freshness check.

TypeScript SDK

verifyWebhookSignature is async and throws FreedamSignatureError when the signature or timestamp is invalid.

import { verifyWebhookSignature, FreedamSignatureError } from '@freedam/sdk';

try {
    await verifyWebhookSignature(
        rawBody,
        request.headers['x-freedam-signature-256'],
        request.headers['x-freedam-timestamp'],
        process.env.FREEDAM_WEBHOOK_SECRET!,
        { maxAgeSec: 300 },
    );
} catch (err) {
    if (err instanceof FreedamSignatureError) {
        return new Response('Invalid signature', { status: 401 });
    }
    throw err;
}

Node.js without the SDK

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValidFreedamWebhook(rawBody: string, signature: string, timestamp: string, secret: string): boolean {
    const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
        return false;
    }

    const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');

    return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

PHP

$timestamp = $_SERVER['HTTP_X_FREEDAM_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_FREEDAM_SIGNATURE_256'] ?? '';
$rawBody = file_get_contents('php://input');

$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);

if (abs(time() - (int) $timestamp) > 300 || ! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

Retries and automatic disabling

Attempt Sent
1 As soon as the event occurs.
2 10 seconds after attempt 1.
3 1 minute after attempt 2.
4 5 minutes after attempt 3.
5 30 minutes after attempt 4. Last attempt.
  • Every failed attempt adds one to the endpoint's failure_count. Any successful delivery resets it to 0.
  • When failure_count reaches 10, the endpoint is set to is_active: false and stops receiving events. Events that occur while it is disabled are not queued for later.
  • To bring an endpoint back, fix your receiver, then click Reset Failures in Admin → API → Webhooks. This sets failure_count to 0 and re-activates the endpoint. Setting is_active: true with PUT also re-activates it, but keeps the failure count, so the next failed attempt disables it again.

Because retries happen, the same event can reach you more than once. Make your handler idempotent, for example by ignoring an asset.updated you have already processed for the same asset_id and timestamp.

Inspecting deliveries

GET /v1/webhooks/{id}/deliveries returns recent delivery records, newest first. Each record includes event_type, payload, attempts, response_status, the first 1000 characters of your response body (or the connection error), delivered_at (set on success), and next_retry_at (set while a retry is pending).

The same health data appears in Admin → API, on the Webhooks tab and in the Overview's webhook health chart.

Disabling and rotating

  • Pause an endpoint by sending is_active: false with PUT. The URL and event list are kept.
  • Rotate the secret when a teammate leaves or after a suspected leak. The response contains the new secret and previous_secret_expires_at, 24 hours later. Deliveries sent after the rotation are signed with the new secret, so deploy it to your receiver right away. If you need a switch without downtime, accept either secret in your receiver until the deploy is done.

show.relatedDocs.heading

show.relatedDocs.subheading