# Webhooks
Source: https://docs.file.ai/deliver/webhooks

> Get a signed POST at your own URL when files, uploads and folders change in your project.

fileAI app

# Webhooks

Copy pageCopy page

Get a signed POST at your own URL when files, uploads and folders change in your project.

Copy pageCopy page

A webhook sends a signed `POST` to an HTTPS URL you own when something happens in your fileAI project, such as a file finishing processing or a File Agent checklist passing. You choose the events. fileAI signs every request with a secret that only you and fileAI know, and records each delivery so you can check it in the app.

You set up webhooks in the fileAI app. There is no public API to create or manage them yet.

## 

[​

](#when-to-use-it)

When to use it

You want to…

Use

Run your own code when files, uploads or folders change, with signed requests

Webhooks

Follow every processing step of one upload, at a URL you set per request

[Processing callbacks](/deliver/callbacks)

Email a person when a document meets your conditions

[Notifications](/deliver/notifications)

Load new and changed files into a warehouse on a schedule

[Incremental loading](/deliver/incremental-loading)

## 

[​

](#how-it-works)

How it works

*   A webhook belongs to one project and receives only that project’s events.
*   Every active webhook subscribed to an event gets its own copy.
*   The body is small: an `eventId`, the `eventCode`, and the IDs of what changed. Use the API to fetch the details.
*   A delivery succeeds when your endpoint returns any `2xx` status. Anything else is recorded as failed and **is not retried**.

## 

[​

](#set-up-a-webhook)

Set up a webhook

1

Open Webhooks

In the fileAI app, select **Developer** in the left sidebar, then open the **Webhooks** tab.

2

Add the webhook

Click **Add Webhook**. Enter a **Name** (3–150 characters) and the **Endpoint URL**. The URL must use HTTPS and be at most 2,048 characters.

3

Choose events

Select at least one event under **Events to send (Automatic)**, **Events to send (File Agent)**, or both. An event can be in only one of the two sections. A greyed-out event isn’t enabled for your project.

4

Save the signing secret

Click **Add webhook**. fileAI shows the **Signing secret**, which starts with `fileai_whsec_`. Click **Confirm and copy** and store it in your secret manager. You can’t view it again. After this, the app shows only a masked value such as `fileai_whsec_****Ab3x`.

To change a webhook, click it in the list. You can edit its name, URL and events, turn off **Active** to pause it, or delete it. Events that happen while a webhook is inactive are not sent later.

### 

[​

](#send-file-agent-events)

Send File Agent events

`folder.checklist.validation.completed` and the File Agent version of `folder.processing.completed` come from a File Agent’s checklist. To receive them:

1.  Subscribe a webhook to the event under **Events to send (File Agent)**.
2.  Open the File Agent’s checklist, turn on **Webhook**, and add the events under **Events**.

While a File Agent uses an event, you can’t delete or deactivate the last active webhook subscribed to it.

## 

[​

](#verify-the-signature)

Verify the signature

Every request carries these headers:

*   `X-FileAI-Timestamp`: when fileAI signed the request, in Unix seconds.
*   `X-FileAI-Signature`: the hex-encoded HMAC-SHA256 of `{timestamp}.{raw body}`, keyed with your signing secret. Use the whole secret, including the `fileai_whsec_` prefix.
*   `X-FileAI-Signature-Version`: `v1`.

1

Read the raw body

Use the request body exactly as received, before you parse it. JSON that has been parsed and serialized again won’t match.

2

Check the timestamp

Reject requests whose `X-FileAI-Timestamp` is far from your clock. We suggest 5 minutes. This stops an old request from being replayed.

3

Compute and compare

Compute HMAC-SHA256 over the timestamp, a `.`, and the raw body, using your signing secret as the key. Hex-encode the result. Compare it to `X-FileAI-Signature` with a constant-time comparison, and reject the request if they differ.

## 

[​

](#example)

Example

Illustrative: shape from the webhook service.

```
POST /fileai/webhooks HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: Mozilla/5.0 (fileAI webhook client/1.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36
X-FileAI-Timestamp: 1761475200
X-FileAI-Signature: 63281caec7c2bde3f5338e1b9ae83385f03aa148d1e0ef1de7cceead9b52d615
X-FileAI-Signature-Version: v1
X-Correlation-ID: corr:7b9e3f52-1c4a-4d8e-9f0b-2a6c5d3e1f40

{"eventId":"0192f3a1-7c2e-7d4b-9a1e-5b3c2d1e0f4a","eventCode":"file.processing.completed","data":{"fileId":"b765bd00-181e-4737-b4d9-004dadd6bd45"}}
```

To test your signature code, sign the timestamp and body above with the test secret `fileai_whsec_dGVzdC1zZWNyZXQtZm9yLWRvY3MtY2hlY2s`. You should get the `X-FileAI-Signature` value shown. Your timestamp check will reject this request as old, which is expected.

A minimal receiver checks the signature, skips events it has already seen, hands the event to a worker, and returns `200`. `already_handled` and `enqueue` stand for your own storage and job queue.

Python

Node.js

```
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SIGNING_SECRET = os.environ["FILEAI_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300

def is_valid_signature(timestamp: str, signature: str, raw_body: bytes) -> bool:
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    signed_payload = timestamp.encode() + b"." + raw_body
    expected = hmac.new(SIGNING_SECRET, signed_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.post("/fileai/webhooks")
def fileai_webhook():
    raw_body = request.get_data()
    if not is_valid_signature(
        request.headers.get("X-FileAI-Timestamp", ""),
        request.headers.get("X-FileAI-Signature", ""),
        raw_body,
    ):
        abort(401)

    event = json.loads(raw_body)
    if not already_handled(event["eventId"]):
        enqueue(event)
    return "", 200
```

```
import crypto from "node:crypto";
import express from "express";

const app = express();
const signingSecret = process.env.FILEAI_WEBHOOK_SECRET;
const toleranceSeconds = 300;

function isValidSignature(timestamp, signature, rawBody) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!/^\d+$/.test(timestamp) || age > toleranceSeconds) {
    return false;
  }
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature, "hex");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

app.post("/fileai/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const valid = isValidSignature(
    req.get("X-FileAI-Timestamp") ?? "",
    req.get("X-FileAI-Signature") ?? "",
    req.body,
  );
  if (!valid) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString("utf8"));
  if (!alreadyHandled(event.eventId)) {
    enqueue(event);
  }
  res.sendStatus(200);
});
```

## 

[​

](#handle-events-safely)

Handle events safely

1

Respond fast

Return a `2xx` within 10 seconds and do the real work in a background job. fileAI waits up to 3 seconds to connect and 10 seconds for the whole request. A slower response is recorded as failed.

2

Skip duplicates

The same event can reach you more than once, with the same `eventId`. Record the `eventId`s you have handled and skip repeats.

3

Expect the same change to fire again

Some events fire again for the same file or folder, each time with a new `eventId`. For example, a file that is reprocessed can finish again, and a folder that already completed can complete again after more files are added. Make your processing safe to run twice for the same `fileId` or `folderId`.

4

Fetch the current state

The body says what changed, not its full state. Before anything consequential, call the API with your key, for example `GET /v1/files/{fileId}/values` or `GET /v1/directories/{directoryId}/checklist-validation`.

5

Keep a fallback

Failed deliveries are not retried. If your endpoint was down, catch up with [incremental loading](/deliver/incremental-loading) or by polling the API.

## 

[​

](#check-deliveries)

Check deliveries

In the app, open **Developer** → **Activity log** → **Webhooks**. Each row shows the date and time, endpoint URL, event code and status. Open a row to see the **Payload Request** fileAI sent and the **Response** your endpoint returned. In the **Webhooks** list, **Last triggered** shows when each webhook last received an event.

## 

[​

](#rotate-the-signing-secret)

Rotate the signing secret

There is no option to regenerate a secret yet. To rotate it:

1.  Add a new webhook with the same URL and events, and save its secret.
2.  Update your endpoint to accept either secret.
3.  Delete the old webhook, then remove the old secret from your endpoint.

While both webhooks exist, each event arrives twice with the same `eventId`. Your duplicate check drops the second copy.

## 

[​

](#reference)

Reference

### 

[​

](#request)

Request

Part

Value

Method

`POST`

`Content-Type`

`application/json`

`User-Agent`

Starts with `Mozilla/5.0 (fileAI webhook client/1.1)`

`X-FileAI-Signature`

Hex HMAC-SHA256 of `{timestamp}.{raw body}`. See [Verify the signature](#verify-the-signature).

`X-FileAI-Timestamp`

Unix seconds when the request was signed.

`X-FileAI-Signature-Version`

`v1`

`X-Correlation-ID`

Identifies this event inside fileAI. Include it when you contact support.

### 

[​

](#body)

Body

Field

Type

Description

`eventId`

string

Unique ID for this event. It is the same for every webhook that receives the event and for repeat deliveries. Use it to skip duplicates.

`eventCode`

string

Which event this is. See [Events](#events).

`data`

object

IDs of what changed. Which fields are present depends on `eventCode`.

The body has no timestamp, workspace ID or project ID. Record the time you received the request if you need one.

### 

[​

](#events)

Events

`eventCode`

Section

Sent when

`data`

`upload.processing.completed`

Automatic

An uploaded file is stored and accepted for processing. Despite the name, this happens **before** OCR and extraction. To know when results are ready, use `file.processing.completed`.

`uploadId`

`file.processing.completed`

Automatic

A file finishes processing. A file that fails processing does not send this event.

`fileId`

`file.processing.retriggered`

Automatic

Someone uses **Re-trigger Callback** in the app on a file whose status is processed or action required.

`fileId`

`file.forensics.completed`

Automatic

fileForensics checks finish for a folder.

`folderId`

`folder.processing.completed`

Automatic

Every file in a folder has finished processing, whether it was processed or failed.

`folderId`, `occurrenceId`

`folder.validation.completed`

Automatic

A workflow finishes validating a subfolder.

`parentFolderId`, `subfolderId`, `workflowId`

`folder.validation.retriggered`

Automatic

Someone re-runs that workflow validation.

`parentFolderId`, `subfolderId`, `workflowId`

`folder.checklist.validation.completed`

File Agent

A File Agent checklist finishes checking a folder, whether it passes or fails.

`folderId`, `checklistId`, `capabilityId`, `checklistResult`

`folder.processing.completed`

File Agent

A File Agent checklist passes for a folder and every file in it has finished processing.

`folderId`, `occurrenceId`

### 

[​

](#data-fields)

Data fields

Field

Description

`uploadId`

The upload’s ID, as returned when you upload. Check it with `GET /v1/uploads/{uploadId}`.

`fileId`

The file’s `fileId` in the API.

`folderId`

The folder’s ID. This is the same value as `directoryId` in the API.

`occurrenceId`

Unique for each time the folder completes. A folder can complete more than once.

`parentFolderId`, `subfolderId`

The validated subfolder and the folder that contains it.

`workflowId`

The workflow that ran the validation.

`checklistId`

The checklist the File Agent ran.

`capabilityId`

The File Agent checklist step that sent the event.

`checklistResult`

`pass` or `fail`.

Example body for each event

upload.processing.completed

folder.processing.completed

folder.checklist.validation.completed

folder.validation.completed

```
{
  "eventId": "0192f3a1-7c2e-7d4b-9a1e-5b3c2d1e0f4a",
  "eventCode": "upload.processing.completed",
  "data": { "uploadId": "e9818d17-e5be-4d57-bdc8-5a40b4f6f4e1" }
}
```

```
{
  "eventId": "0192f3a2-1b4d-7e6f-8a9b-0c1d2e3f4a5b",
  "eventCode": "folder.processing.completed",
  "data": {
    "folderId": "6650f0c2a1b2c3d4e5f60718",
    "occurrenceId": "3f2a9c1e-5b7d-4e8f-a0b1-c2d3e4f5a6b7"
  }
}
```

```
{
  "eventId": "0192f3a3-9d8c-7b6a-9f0e-1d2c3b4a5f6e",
  "eventCode": "folder.checklist.validation.completed",
  "data": {
    "folderId": "6650f0c2a1b2c3d4e5f60718",
    "checklistId": "66a1b2c3d4e5f60718293a4b",
    "capabilityId": "8d7c6b5a-4f3e-4d2c-9b1a-0f9e8d7c6b5a",
    "checklistResult": "pass"
  }
}
```

```
{
  "eventId": "0192f3a4-2e3f-7a4b-8c5d-6e7f8a9b0c1d",
  "eventCode": "folder.validation.completed",
  "data": {
    "parentFolderId": "6650f0c2a1b2c3d4e5f60718",
    "subfolderId": "6650f0c2a1b2c3d4e5f60719",
    "workflowId": "66b2c3d4e5f60718293a4b5c"
  }
}
```

### 

[​

](#delivery-behavior)

Delivery behavior

Behavior

Value

Success

Any `2xx` response

Connect timeout

3 seconds

Total timeout

10 seconds

Retries

None. A failed delivery is not sent again.

Order

Not guaranteed

Response kept

The first 64 KB of your response body, shown in the Activity log

Redirects

Avoid them. Point the webhook at the final URL. A `301` or `302` turns the `POST` into a `GET` without the body.

### 

[​

](#troubleshooting)

Troubleshooting

Symptom

Check

Nothing arrives

The webhook is **Active**, it is subscribed to the event, and the event comes from the same project. For File Agent events, the checklist’s **Webhook** is also on.

Signature doesn’t match

You signed the raw body, not re-serialized JSON. You used the full secret, including `fileai_whsec_`, and the secret belongs to this webhook.

Failed with no status code

fileAI couldn’t connect within 3 seconds or got no response within 10 seconds.

## 

[​

](#related)

Related

## Processing callbacks

Follow every processing step of one upload at a URL you set per request.

## Incremental loading

Catch up on changes you missed by pulling them on a schedule.

## Async jobs

Poll uploads and jobs when you need a fallback.

## Notifications

Email people when a document meets your conditions.

[Previous](/deliver/notifications)[

Checklist

What to set up before you move a working prototype to production.

Next

](/production)

⌘I

## On this page

Back to top

*   [When to use it](#when-to-use-it)
*   [How it works](#how-it-works)
*   [Set up a webhook](#set-up-a-webhook)
    *   [Send File Agent events](#send-file-agent-events)
*   [Verify the signature](#verify-the-signature)
*   [Example](#example)
*   [Handle events safely](#handle-events-safely)
*   [Check deliveries](#check-deliveries)
*   [Rotate the signing secret](#rotate-the-signing-secret)
*   [Reference](#reference)
    *   [Request](#request)
    *   [Body](#body)
    *   [Events](#events)
    *   [Data fields](#data-fields)
    *   [Delivery behavior](#delivery-behavior)
    *   [Troubleshooting](#troubleshooting)
*   [Related](#related)
