Gareth Wilson Gareth Wilson

Guide to Airweave Webhooks: Features and Best Practices

Published


Airweave connects SaaS apps, databases and file stores to AI agents. It syncs each source into a searchable collection, and its webhooks tell you how those syncs are going: when a job is queued, starts, finishes with entity counts, fails or is cancelled, and when source connections and collections are created, authenticated or removed. Teams use them to refresh search indexes after a sync, alert on failures, and show sync status in their own product without polling the API. The webhooks are in beta.

What are Airweave webhooks?

An Airweave webhook is a subscription created through the API: a URL plus a list of event types. Delivery runs on Svix, which signs each request with a per-subscription secret and adds svix-id, svix-timestamp and svix-signature headers. The signature is Standard Webhooks HMAC-SHA256.

Because Svix handles delivery, Airweave inherits its retry and recovery tools. A failed message is retried five times over about two and a half hours, and anything still undelivered can be resent through Airweave's recover endpoint once your receiver is healthy again.

Airweave webhook features

FeatureDetails
StatusBeta
ConfigurationSubscriptions API (/webhooks/subscriptions), admin or owner role with Bearer auth
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature, with svix-id and svix-timestamp
SecretPer subscription, whsec_..., read with include_secret=true
BodyJSON, Content-Type: application/json
Event routingBody field event_type
SuccessAny 2xx. 4xx, 5xx and a 30-second timeout are retried
Retries5 attempts: immediate, 5 seconds, 5 minutes, 30 minutes, 2 hours. Then the message is marked failed
RecoveryPOST /webhooks/subscriptions/{id}/recover, or recover_since when re-enabling a subscription
DeliveryAt least once, not ordered. Deduplicate on the svix-id header
Source IPsNot published

Events and payloads

EventTriggered WhenCommon Use Cases
sync.pendingA sync job is created and queuedShow "queued" state
sync.runningThe sync job starts processingProgress display
sync.completedThe sync job finishes successfully; carries entity and chunk countsTrigger downstream workflows, refresh search
sync.failedThe sync job hit an error; error has the detailsAlert, re-authenticate the source
sync.cancelledThe sync job was cancelled manuallyClear "running" state
source_connection.createdA source connection record is created (OAuth may be unfinished, see is_authenticated)Track onboarding
source_connection.auth_completedThe OAuth flow completed and the connection is readyStart the first sync, tell the user
source_connection.deletedA source connection and its data were removedClean up references
collection.createdA collection was createdRegister it in your app
collection.updatedCollection properties changedRefresh cached names
collection.deletedA collection and its data were removedRemove references

Each domain has its own flat body. They share event_type and timestamp and add the IDs for that domain:

DomainFields besides event_type and timestamp
Syncsync_id, sync_job_id, collection_id, collection_readable_id, collection_name, source_connection_id, source_type. sync.completed adds entities_inserted, entities_updated, entities_deleted, entities_skipped, chunks_written; sync.failed adds error
Source connectionsource_connection_id, collection_readable_id, source_type, is_authenticated
Collectioncollection_id, collection_name, collection_readable_id

This is the collection.created sample from the webhook samples library:

{
  "event_type": "collection.created",
  "collection_id": "770e8400-e29b-41d4-a716-446655440002",
  "collection_name": "Finance Data",
  "collection_readable_id": "finance-data-ab123",
  "timestamp": "2025-01-15T13:50:00Z"
}

The delivered body has no event ID. Airweave's message-history API shows an event_id, but it isn't in the body, so deduplicate on svix-id, which stays the same across retries. The same history example shows job_id and status in a sync payload where the event pages use sync_job_id, so read IDs defensively.

source_connection.created can fire before the user finishes OAuth. Check is_authenticated, or wait for source_connection.auth_completed before you start anything that needs the connection.

Setting up Airweave webhooks

Creating, updating, deleting and recovering subscriptions needs the admin or owner role and Bearer token authentication. An API key is enough for read-only calls such as listing subscriptions, messages and delivery attempts. The URL must be HTTPS, except for local development.

Create the subscription with the events you want:

curl -X POST 'https://api.airweave.ai/webhooks/subscriptions' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://your-server.com/webhooks/airweave",
    "event_types": ["sync.completed", "sync.failed", "source_connection.created", "collection.deleted"]
  }'

The response holds the subscription id (ep_...), url, filter_types and disabled: false. Then read its signing secret:

curl -X GET 'https://api.airweave.ai/webhooks/subscriptions/{subscription_id}?include_secret=true' \
  -H 'x-api-key: YOUR_API_KEY'

The secret field holds a whsec_... value. Store all of it, prefix included, as AIRWEAVE_WEBHOOK_SECRET.

You can pass your own secret (minimum 24 characters) when you create the subscription. Even then, use the whsec_ secret Airweave returns for the subscription, not the value you sent, both in your code and in a Hookdeck Airweave source.

To put Hookdeck in front, create a source with the Airweave type, paste that whsec_ value as its verification secret, and use the source URL as the subscription url. For local testing, forward the source to your machine with the Hookdeck CLI:

npx hookdeck-cli listen 3000 airweave --path /webhooks/airweave

Securing Airweave webhooks

The signed content is <svix-id>.<svix-timestamp>.<raw body>. The HMAC key is the base64-decoded part of the secret after whsec_, and svix-signature can hold several space-separated v1,<base64> entries, any one of which may match.

Airweave's manual-verification snippets split the header on , and take the last space-separated token. That works while there is one signature, but the header format allows several entries, and the snippet would then check only the last. The verifier below checks every v1 entry and falls back to webhook-* headers if svix-id is missing, as the Hookdeck source does:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Airweave recommends the svix library for production. It parses the header correctly but rejects timestamps more than five minutes old, and a request that Hookdeck retries or replays still carries its original svix-timestamp. Hookdeck itself applies no timestamp tolerance, so behind Hookdeck use a verifier like this one, or widen the library's window.

The receiver below refuses a secret without the whsec_ prefix, since a custom value would verify against the wrong key, and treats sync outcomes as the events worth storing:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// storeEvent(svixId, event) persists the event and resolves once it is durable.
function createAirweaveReceiver(storeEvent) {
  const app = express();
  app.post('/webhooks/airweave', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.AIRWEAVE_WEBHOOK_SECRET;
    if (!secret || !secret.startsWith('whsec_')) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    const deliveryId = req.headers['svix-id'] || req.headers['webhook-id'];
    try {
      await storeEvent(deliveryId, event);
    } catch {
      return res.sendStatus(503); // retried on Airweave's 5-attempt schedule
    }
    // Handle sync.completed and sync.failed in a worker; acknowledge everything else too.
    return res.sendStatus(200);
  });
  return app;
}

A Hookdeck Airweave source answers every request with 200, including ones whose signature fails, and marks the failures in its request log instead of forwarding them.

Make Airweave webhooks production-ready. Hookdeck Event Gateway verifies each Svix-signed sync and collection event, keeps the full delivery history, and lets you retry or replay events to your indexer after an outage.

Airweave webhook limitations and pain points

A custom secret isn't the key

The Problem: You set your own secret when you created the subscription, used the same value in your verifier, and every delivery fails.

Why It Happens: Svix stores the custom value and reports the subscription's secret back in whsec_ form. That returned value is what signs requests.

Workarounds:

  • Always fetch the subscription with include_secret=true and use the whsec_ value it returns.

How Hookdeck Can Help: Hookdeck's configuration alert for the Airweave source gives the same instruction, and its request log shows whether verification passed for each delivery as soon as you send a test sync.

Five attempts, then nothing until you recover

The Problem: After a few hours of downtime, sync events stop arriving and don't come back on their own.

Why It Happens: The fifth attempt comes two hours after the fourth. After that the message is marked failed and Airweave stops trying it. A subscription that has been disabled, by you or through the API, sends nothing at all.

Workarounds:

  • Check disabled on the subscription when deliveries stop.
  • Call POST /webhooks/subscriptions/{id}/recover, or pass recover_since when you re-enable it, to resend failed messages.

How Hookdeck Can Help: Hookdeck accepts each event as it arrives, so your outage doesn't use up Airweave's five attempts. It then retries delivery to your service on a schedule you set.

Events are unordered

The Problem: A late sync.running overwrites the sync.completed you already stored, and the sync looks stuck.

Why It Happens: Delivery is at least once and Airweave doesn't guarantee order, so a retried earlier event can arrive after a later one.

Workarounds:

  • Track state per sync_job_id and only move forward: pending, running, then a final state.
  • Compare timestamp before overwriting.

How Hookdeck Can Help: Every delivery is logged with its arrival time and headers, which makes out-of-order cases easy to find when a job's status looks wrong.

Beta event surface

The Problem: A new event type or a renamed field breaks a strict handler.

Why It Happens: Airweave labels webhooks as beta, and its docs already disagree on some field names (job_id against sync_job_id).

Workarounds:

  • Return 200 for any event_type you don't handle. Airweave retries 4xx responses too.
  • Read IDs with fallbacks rather than assuming one name.

How Hookdeck Can Help: A transformation can normalise field names in one place before events reach your handlers.

Best practices

Act on sync.completed, not sync.running

sync.completed carries entities_inserted, entities_updated, entities_deleted, entities_skipped and chunks_written. Use those counts to decide whether a search cache needs refreshing, and skip the work when everything was skipped.

Treat sync.failed as an action item

error explains what went wrong. If the error comes from the source's credentials, the user has to re-authenticate the connection, so route those failures to someone who can.

Deduplicate on svix-id

With no event ID in the body, the svix-id header is the stable key across retries. Store it before processing. Implementing webhook idempotency walks through the storage side.

Answer inside 30 seconds

A slow handler times out and is retried. Store the event, return 200, and run re-indexing or notifications afterwards. Processing webhooks asynchronously explains why.

Conclusion

Airweave reports the lifecycle of syncs, source connections and collections through Svix, with a recover endpoint behind it. The events are still in beta and arrive unordered, so write handlers that tolerate both.

If you would rather not depend on five retries over two and a half hours, Hookdeck Event Gateway can accept Airweave's events immediately and deliver them to your service when it is ready. Sign up for Hookdeck and point a subscription at a new source.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.