# Guide to Fivetran Webhooks: Features and Best Practices

Fivetran webhooks tell your systems what your data pipelines are doing: a connection starts or finishes a sync, a transformation succeeds or fails, a connection test fails, or someone pauses, edits or deletes a connector. If you run downstream jobs after a sync, such as dbt models, cache refreshes or alerts, webhooks let them start the moment the data lands instead of on a schedule.

This guide covers how Fivetran webhooks are created and signed, the events and payload, the test request that can stop a webhook from being created, the retry and deactivation rules, and the best practices for running a Fivetran receiver in production.

## What are Fivetran webhooks?

A Fivetran webhook is a URL, a list of events and an optional secret, created through the REST API at account level or for a single group. There is no dashboard screen for webhooks. When a subscribed event happens, Fivetran sends a JSON `POST` to the URL.

Signing is opt-in. Fivetran's docs say "the webhook payload can be signed if you specified a secret during the webhook creation", using "the SHA-256 HMAC algorithm". A webhook created without a secret sends unsigned requests. The page itself does not name the header or the encoding. Both come from Fivetran's example receivers, which read the `x-fivetran-signature-256` header and compare it to a hex digest of the raw body.

## Fivetran webhook features

| Feature | Details |
| --- | --- |
| Configuration | REST API only: `POST /v1/webhooks/account` or `POST /v1/webhooks/group/{groupId}` |
| Subscription model | You choose the events per webhook |
| URL | Must be HTTPS. An HTTP URL is refused with `400 Bad Request` |
| Creation check | One test `POST` on create or activate. If it fails, the webhook is not created |
| Signature header | `X-Fivetran-Signature-256`, only when a secret is set |
| Scheme | HMAC-SHA256, hex digest. Fivetran's examples compare it as uppercase |
| Signed content | The raw request body, nothing else |
| Replay window | None. No timestamp is sent or signed |
| Response window | 10 seconds, expecting `200 OK` |
| Retries | Up to five retries over 24 hours |
| Auto-deactivation | After more than three days of only non-2xx responses |
| Test events | `POST /v1/webhooks/{webhookId}/test` with an event name |

Fivetran publishes no source IP list for webhooks, so set a secret and verify the signature.

## Common events

Fivetran supports 16 events:

| Area | Events | Fires when |
| --- | --- | --- |
| Syncs | `sync_start`, `sync_end` | A connection sync starts or ends. `data.status` on `sync_end` is `SUCCESSFUL` or `FAILED` |
| Transformations | `transformation_start`, `transformation_succeeded`, `transformation_failed` | A transformation run starts, succeeds or fails |
| Connection tests | `connection_successful`, `connection_failure` | A connection's setup test passes or fails |
| Connector lifecycle | `create_connector`, `pause_connector`, `resume_connector`, `edit_connector`, `delete_connector` | A connector is created, paused, resumed, edited or deleted |
| Resyncs | `force_update_connector`, `resync_connector`, `resync_table` | A sync is forced, or a connector or table is resynced |
| Deprecated | `status` | Marked "(deprecated)" in Fivetran's list, with no replacement named |

Fivetran publishes one example payload, for `sync_end`:

```json
{
  "event": "sync_end",
  "created": "2021-08-18T11:38:34.386Z",
  "connector_type": "asana",
  "connector_id": "mystified_presiding",
  "connector_name": "mystified_presiding",
  "sync_id": "mystified_presiding",
  "destination_group_id": "deck_enjoy",
  "data": {
    "status": "SUCCESSFUL"
  }
}

```

The top-level fields are common to every event. `event` is the event type, `created` is when the event was generated, and the `connector_*`, `sync_id` and `destination_group_id` fields identify where it came from. Fivetran says the `data` object's fields "vary depending on the event type", and it documents no other shape, so read `data` defensively. There is no event ID.

## Setting up Fivetran webhooks

Start your receiver first. Fivetran tests the URL as part of creating the webhook.

1. Generate a random secret and store it server-side, for example as `FIVETRAN_WEBHOOK_SECRET`.
2. Create the webhook with your Fivetran API key and secret as Basic auth credentials:

```bash
curl -X POST "https://api.fivetran.com/v1/webhooks/account" \
  -u "$FIVETRAN_API_KEY:$FIVETRAN_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/fivetran",
    "events": ["sync_start", "sync_end", "connection_failure"],
    "active": true,
    "secret": "'"$FIVETRAN_WEBHOOK_SECRET"'"
  }'

```

For one group only, use `POST /v1/webhooks/group/{groupId}` with the same body.

1. Fivetran sends one test `POST` to the URL. If the endpoint "is not reachable or returns an unsuccessful status code, Fivetran does not create the webhook and returns an HTTP 400 Bad Request error". You can create the webhook with `"active": false` to skip the test, but it runs again when you activate it.
2. Send a test event whenever you need one:

```bash
curl -X POST "https://api.fivetran.com/v1/webhooks/$WEBHOOK_ID/test" \
  -u "$FIVETRAN_API_KEY:$FIVETRAN_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"event": "sync_end"}'

```

For local development, the [Hookdeck CLI](/docs/cli) (`hookdeck listen 3000 fivetran --path /webhooks/fivetran`) gives you a public HTTPS URL that forwards to your local server, with no account required.

## Securing Fivetran webhooks

Read `X-Fivetran-Signature-256`, compute an HMAC-SHA256 of the raw body with your secret as the key, encode it as hex, and compare in constant time. Fivetran's Flask and Express examples both upper-case the computed digest and the header before comparing, so the case of the hex on the wire is not documented. Normalise both sides to the same case.

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

function verifyFivetranSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex').toUpperCase();
  const received = Buffer.from(signatureHeader.trim().toUpperCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/fivetran', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyFivetranSignature(req.body, req.get('x-fivetran-signature-256'), process.env.FIVETRAN_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Queue work keyed on event.event, then answer with 200 OK.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

def verify_fivetran_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest().upper()
    received = signature_header.strip().upper().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_fivetran_signature(raw_body, request.headers.get("x-fivetran-signature-256"),
#                           os.environ["FIVETRAN_WEBHOOK_SECRET"])

```

To check your verifier, use the secret `fivetran-test-secret` and this exact body:

```
{"event":"sync_end","created":"2021-08-18T11:38:34.386Z","connector_id":"mystified_presiding","data":{"status":"SUCCESSFUL"}}

```

The signature should be this, in either case:

```
e1d81e7016efd5c28fca5e778a9a2a3d8f7778f0291116f86513b6fd1004e418

```

Fivetran does not say whether the test request sent on creation is signed. If your receiver rejects unsigned requests, check its logs when you create a webhook.

> **Make Fivetran webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `X-Fivetran-Signature-256` in either hex case, answers the creation test with a `200`, and durably queues every sync and transformation event before your service sees it.

## Fivetran webhook limitations and pain points

### The webhook won't create

**The Problem:** The create call returns `400 Bad Request` and no webhook exists.

**Why It Happens:** Fivetran sends a test `POST` during creation and refuses to create the webhook unless it gets a successful response. An HTTP URL, a server that is not running yet, a tunnel that has closed, or a handler that rejects the test body all cause it.

**Workarounds:**

* Use an HTTPS URL and start the endpoint before calling the API.
* Make sure your handler returns `200` for any well-formed request it does not recognise.
* Create with `"active": false` if you need the webhook record first, then activate it once the endpoint is live.

**How Hookdeck Can Help:** A Hookdeck source answers Fivetran's test request with a `200` straight away, whether or not your service is running, so the webhook is created and you can inspect the test body in the request log.

### A missing secret means no signature

**The Problem:** Requests arrive with no `X-Fivetran-Signature-256` header, so there is nothing to verify.

**Why It Happens:** Signing only happens when a secret was set at creation. The secret cannot be read back from the API either.

**Workarounds:**

* Always pass `secret` on creation.
* If you lost it, set a new one with the modify-webhook endpoint (`PATCH /v1/webhooks/{webhookId}`) and update your environment at the same time.
* If you run a webhook without a secret, treat its requests as unauthenticated and re-check anything important against the Fivetran API.

**How Hookdeck Can Help:** Hookdeck verifies the signature when you give the source your secret. If you didn't set a secret in Fivetran, leave verification off on the source.

### Five retries in 24 hours, then the event is gone

**The Problem:** A sync finished during an outage, the retries ran out, and the job that should have followed it never ran.

**Why It Happens:** Fivetran times out after 10 seconds and retries "for up to 24 hours in increasing time intervals": 6 minutes, 21 minutes, then roughly 1h18, 4h40 and 16h48 later in its worked example, five retries in all. After that it stops sending that event instance. Each instance is retried on its own, so the next sync's events keep arriving while an earlier one is still failing. If a webhook fails with only non-2xx responses for more than three days, Fivetran deactivates it.

**Workarounds:**

* Answer quickly and queue the downstream work.
* After an outage, check sync history through the Fivetran API rather than relying on webhooks you may have missed.
* If a webhook is deactivated, fix the endpoint and update it with `"active": true`, which runs the creation test again.

**How Hookdeck Can Help:** Hookdeck acknowledges every delivery and stores it, then retries delivery to your service on [your own schedule](/docs/retries). Events that arrive during your downtime are held and can be replayed, so the 24-hour window stops being the limit.

### `sync_end` doesn't mean success

**The Problem:** A dbt run or an alert fires after a failed sync.

**Why It Happens:** `sync_end` fires whether the sync succeeded or failed. The outcome is in `data.status`.

**Workarounds:**

* Branch on `data.status`, and only trigger downstream jobs for `SUCCESSFUL`.
* Subscribe to `connection_failure` and `transformation_failed` for alerting.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can route `sync_end` events with `data.status` of `FAILED` to an alerting destination and successful ones to your job runner.

## Best practices

### Verify against the raw body

Mount `express.raw()` on the route, read `await request.body()` in FastAPI, or use `Buffer.from(await request.arrayBuffer())` in a Next.js route handler. Parse only after verifying.

### Subscribe to what you use

Fivetran recommends subscribing only to the events you need, which "minimizes the number of HTTPS requests to your server". You can change the event list later with the modify-webhook endpoint.

### Build your own idempotency key

There is no event ID. Combine `event`, `connector_id`, `sync_id` and `created` into a key and store it before processing. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Fivetran waits 10 seconds for a `200 OK`. Verify, queue and answer, and run the downstream jobs from the queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Read `data` defensively

Only the `sync_end` shape is documented. Use optional access on `data` and log events whose shape you have not seen before, so you learn their fields from real traffic.

## Conclusion

Fivetran webhooks are created only through the REST API, tested on creation, and signed only when you supply a secret. Set a secret, verify `X-Fivetran-Signature-256` against the raw body without caring about hex case, branch on `data.status` for `sync_end`, and answer within 10 seconds. With five retries in 24 hours, plan for the event you might miss.

[Hookdeck Event Gateway](https://hookdeck.com) answers the creation test, verifies each signature and holds every event until your service has processed it, so a pipeline outage on your side does not drop the signal that a sync finished.

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