# Guide to Fiber AI Webhooks: Features and Best Practices

Fiber AI is a B2B data API for finding and enriching people and companies. Many of its operations run in the background, including batch enrichments, lookups, searches, audience builds and Tracker monitoring, so webhooks are how your system learns that a job has finished, whether it succeeded, and where to find the results.

This guide covers how Fiber AI webhooks are configured and signed, the events and their payloads, the retry schedule, and the practices that keep a Fiber AI receiver reliable.

## What are Fiber AI webhooks?

Fiber AI delivers webhooks through Svix. You add an HTTPS endpoint, pick the event types it should receive, and Fiber AI posts a JSON body to it when one of those events happens. Svix handles the delivery itself: retries with backoff, signatures, a self-service portal with delivery logs, and one-click replay.

Every delivery carries `svix-id`, `svix-timestamp` and `svix-signature` headers. The signature is an HMAC-SHA256 of the ID, the timestamp and the raw body, keyed with the endpoint's `whsec_...` signing secret. This is the same construction as Standard Webhooks, with `svix-` header names.

## Fiber AI webhook features

| Feature | Details |
| --- | --- |
| Delivery platform | Svix |
| Configuration | Portal via `fiber.ai/app/webhooks`, or the webhook endpoints API |
| Endpoints | HTTPS only, subscribed to at least one event type |
| Payload | Varies by event; most completion events carry `success`, an identifier and `error` |
| Signature headers | `svix-id`, `svix-timestamp`, `svix-signature` |
| Scheme | `v1,<base64 HMAC-SHA256>`, space-separated entries |
| Signed content | `{svix-id}.{svix-timestamp}.{raw body}` |
| Secret | `whsec_...`, per endpoint, shown only at create and rotate |
| Rotation | The old secret keeps verifying for a short grace period |
| Timeout | 15 seconds; `3xx` counts as a failure |
| Retries | Immediately, then 5s, 5min, 30min, 2h, 5h, 10h, 10h |
| Ordering | Not guaranteed |
| Source IPs | A published set, available on request |

## Common events

Fiber AI's catalogue has 26 event types. The docs group 25 of them, and the endpoint API also accepts `batch_contact_enrich_v1.completed`.

| Area | Events | Fires when |
| --- | --- | --- |
| Enrichment | `reveal.completed`, `batch_live_enrich.completed`, `batch_contact_enrich.completed`, `mosaic.completed`, `audience.enrichment_completed`, `audience.prospect_export_completed`, `audience.company_export_completed` | A contact reveal, enrichment, file repair or audience export finishes |
| Lookup | `github_to_linkedin.completed`, `github_lookup.completed`, `domain_lookup.completed`, `social_media_lookup.completed` | A lookup job finishes |
| Search | `saved_search.run_upcoming`, `saved_search.run_completed`, `combined_search.completed`, `google_maps_search.completed`, `local_business_search.completed`, `audience.build_completed` | A saved search is about a day away or has run, or an async search or audience build finishes |
| Monitoring | `tracker.signal_detected`, `tracker.list_run_upcoming`, `tracker.list_run_completed`, `job.changed`, `job_changes.profiles_added` | A Tracker rule fires, a tracker list refresh is due or done, or tracked people change jobs |
| Other | `depth_chart.completed`, `sales_nav_scrape.completed`, `sales_nav_lite_scrape.completed` | A depth chart or Sales Navigator scrape finishes |

The tracker list run events are turned on per organisation. Subscribing an endpoint isn't enough; ask Fiber AI to enable them.

Here is Fiber AI's `reveal.completed` example:

```json
{
  "task_id": "abc123",
  "success": true,
  "emails": [{ "emailAddress": "jane@company.com", "type": "work" }],
  "phone_numbers": [{ "phoneNumber": "+1-555-0100", "type": "mobile" }],
  "linkedin_url": "https://www.linkedin.com/in/jane-doe"
}

```

* `success` says whether the job worked, and `error` is filled in when it didn't.
* The identifier depends on the operation: `task_id`, `run_id`, `search_id` or `audience_id`.
* `job.changed` is different. It carries a `profiles` array with no `success` or `error`, and each profile includes the `listId` it was tracked on, so one person can arrive once per list.
* `batch_live_enrich.completed` reports counts (`completed`, `failed`, `not_found`, `malformed`) instead of `error`.
* The heads-up events (`saved_search.run_upcoming`, `tracker.list_run_upcoming`, `tracker.list_run_completed`) only ever send `success: true`.
* Tracker signals use a richer envelope with the matched entity, the rule, a stable `eventId` and `occurredAt`.

The documented example has no field naming the event type, and Fiber AI documents no event header. Call `listWebhookEventTypes` for each type's `examplePayload`, or send yourself test events, before deciding how to route.

## Setting up Fiber AI webhooks

1. Go to `fiber.ai/app/webhooks` and click **Set up webhooks**. Always enter the portal from there, because portal links are short-lived.
2. Click **Add Endpoint** and enter your HTTPS URL.
3. Select the event types to receive and save. Delivery starts immediately.
4. Copy the endpoint's signing secret (`whsec_...`) and store it server-side, for example as `FIBER_AI_WEBHOOK_SECRET`.

You can do the same over the API, free of credits:

```bash
curl -X POST "https://api.fiber.ai/v1/webhooks/endpoints" \
  -H "Content-Type: application/json" \
  -d '{"apiKey": "'"$FIBER_API_KEY"'",
       "url": "https://example.com/webhooks/fiber",
       "eventTypes": ["reveal.completed", "batch_contact_enrich.completed"]}'

```

The response's `output.signingSecret` is only returned here and by `rotateWebhookEndpointSecret`. Reads never return it.

If you receive Fiber AI webhooks through Hookdeck, use the Hookdeck source URL as the endpoint and paste that endpoint's `whsec_...` secret into the Fiber AI source. Each endpoint has its own secret, so if you create several endpoints, give each one its own Hookdeck source.

To test, use `sendTestWebhookEvent` or the portal's **Send Example** button. Both deliver an example payload of the type you choose, signed like a real one.

## Securing Fiber AI webhooks

Join `svix-id`, `svix-timestamp` and the raw request body with dots. Take the part of the secret after `whsec_`, base64-decode it, and use those bytes as the HMAC-SHA256 key. Base64-encode the result and compare it in constant time with each `v1,` entry in the space-separated `svix-signature` header. A match on any entry is enough.

Fiber AI's docs, and the Fiber AI skill's examples, use the official `svix` library. It always rejects an `svix-timestamp` more than 5 minutes from your clock, and the window can't be turned off. Hookdeck retries and replays resend the original `svix-timestamp`, so a delivery that reaches your code later than that fails even though its signature is valid. If you receive through Hookdeck, use a manual verifier like the one below, or the one in the skill's `references/verification.md`, where the window is optional and off by default.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyFiberSignature(rawBody, headers, secret, toleranceSeconds = 0) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader || !secret || !secret.startsWith('whsec_')) return false;
  if (!/^\d+$/.test(timestamp)) return false;

  // Optional replay check, off by default.
  if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
    return false;
  }

  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest();

  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/fiber', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.FIBER_AI_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyFiberSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on req.headers['svix-id'], then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import binascii
import hashlib
import hmac
import time

def verify_fiber_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 0) -> bool:
    msg_id = headers.get("svix-id")
    timestamp = headers.get("svix-timestamp")
    signature_header = headers.get("svix-signature")
    if not (msg_id and timestamp and signature_header and secret and secret.startswith("whsec_")):
        return False
    if not timestamp.isdigit():
        return False
    # Optional replay check, off by default.
    if tolerance_seconds > 0 and abs(time.time() - int(timestamp)) > tolerance_seconds:
        return False

    try:
        key = base64.b64decode(secret[len("whsec_"):])
    except (binascii.Error, ValueError):
        return False
    signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
            return True
    return False

# In FastAPI: raw_body = await request.body()
# verify_fiber_signature(raw_body, request.headers, os.environ["FIBER_AI_WEBHOOK_SECRET"])

```

To check your verifier, use the secret `whsec_ZmliZXItYWktZ3VpZGUtdGVzdC1zaWduaW5nLWtleSE=`, the ID `msg_2nQx7bP0uYc3LwKfR8sT1vZ5aE9`, the timestamp `1761400000` and this exact body:

```
{"task_id":"abc123","success":true,"linkedin_url":"https://www.linkedin.com/in/jane-doe"}

```

The `svix-signature` value should be:

```
v1,b8/3JsNDYwrxzxBJuq2ImhIzv8SPtBNv03e8LsNMjoQ=

```

> **Make Fiber AI webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the Svix signature on every Fiber AI delivery, answers well inside the 15-second timeout, and holds each job result until your service has processed it.

## Fiber AI webhook limitations and pain points

### The body doesn't name the event

**The Problem:** One handler receives several event types and can't tell which one arrived.

**Why It Happens:** Fiber AI documents no event-type header, and its documented example body has no type field. Payload shapes also overlap: most completion events carry `success`, an identifier and `error`.

**Workarounds:**

* Check `examplePayload` from `listWebhookEventTypes`, or a test delivery, for each type you subscribe to.
* Create a separate endpoint for each event type, or each group you handle the same way, with its own URL path, so the route tells you the type.

**How Hookdeck Can Help:** Give each Fiber AI endpoint its own Hookdeck source and route each one to the right handler. Hookdeck [filters](/docs/filters) can also route on fields such as `profiles` or `task_id` when one source receives several types.

### The library's fixed 5-minute window

**The Problem:** Genuine events fail verification when they are retried or replayed late.

**Why It Happens:** The `svix` libraries reject any `svix-timestamp` more than 5 minutes from the current time, and a retry or replay from Hookdeck keeps the original timestamp because it is part of the signed content.

**Workarounds:**

* Verify manually and choose your own window, or drop the window and deduplicate on `svix-id`.
* Run verification as soon as the request arrives, before it is queued.

**How Hookdeck Can Help:** Hookdeck verifies the signature when the request arrives and applies no timestamp window, so retrying or replaying an event from Hookdeck doesn't fail verification at the source.

### Out-of-order and repeated deliveries

**The Problem:** A later result overwrites a newer one, or the same job is processed twice.

**Why It Happens:** Fiber AI doesn't guarantee order, and retries over roughly a day mean the same message can arrive more than once. People tracked on several job-change lists arrive once per list.

**Workarounds:**

* Deduplicate on `svix-id`, which stays the same across retries. Tracker signals also carry a stable `eventId`.
* Order Tracker signals by `occurredAt`, not by arrival.
* Treat `job.changed` entries per `listId`.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated deliveries on `svix-id`, and its delivery queue smooths bursts from large batch jobs.

### Secrets shown only twice

**The Problem:** A lost secret means you can't verify deliveries until you rotate it.

**Why It Happens:** Fiber AI returns the signing secret only when an endpoint is created or its secret is rotated.

**Workarounds:**

* Save the secret into your secrets manager in the same step that creates the endpoint.
* To recover, rotate the secret and deploy the new one during the grace period, while the old one still verifies.

**How Hookdeck Can Help:** With Hookdeck in front, only the Fiber AI source holds the secret, so a rotation means updating one field.

## Best practices

### Verify against the raw body

The signature covers the exact bytes. Use `express.raw({ type: '*/*' })` in Express, `await request.text()` in a Next.js route handler and `await request.body()` in FastAPI, and parse only after verification.

### Check `success` before using results

A completion event can report a failed job. Branch on `success`, log `error`, and only fetch or store results for jobs that worked.

### Pause endpoints during maintenance

Update an endpoint with `disabled: true` to stop deliveries without deleting it, and set it back to `false` to resume. A message that exhausts its retries is marked failed in the portal, where you can retry it manually.

### Acknowledge fast and process asynchronously

Verify, store, return `200`, and do the work afterwards. Slow handlers hit the 15-second timeout and cause needless retries. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Fiber AI webhooks arrive through Svix, signed with the `svix-*` headers and a per-endpoint `whsec_` secret. Save the secret when you create the endpoint, verify against the raw body with a verifier whose window suits your retry path, work out how you will tell event types apart, and deduplicate on `svix-id`.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks every Fiber AI signature, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Fiber AI webhooks reliably in minutes.