# Processing callbacks
Source: https://docs.file.ai/deliver/callbacks

> The payload fileAI POSTs to your callback URL as each processing step of an upload finishes.

fileScout

# Processing callbacks

Copy pageCopy page

The payload fileAI POSTs to your callback URL as each processing step of an upload finishes.

Copy pageCopy page

A processing callback is a `POST` that fileAI sends to your callback URL as an upload moves through processing. It tells you which step finished, whether it succeeded, and, once the upload has been split into documents, the `fileId` of each document, so you can fetch values without polling. This page is the payload reference. To choose between callbacks and polling, and for extract, split, classify and agent job callbacks, see [Async jobs](/api/async-jobs). For signed events about files, uploads, folders and File Agents, set up [webhooks](/deliver/webhooks).

Upload callbacks are **sent once and not retried**. If your endpoint is down or returns an error, that event is lost. Keep polling `GET /v1/uploads/{uploadId}` as a fallback. See [Async jobs](/api/async-jobs).

## 

[​

](#how-it-works)

How it works

### 

[​

](#where-the-callback-goes)

Where the callback goes

You set `callbackURL` when you request the upload, or rely on your API key’s default callback URL. Multipart uploads do not fall back to the key’s default. The full rules are under [Upload options](/ingest/uploads#upload-options). Files added in the fileAI app or by email use your organization’s file-processing callback URL, if one is set. Use an HTTPS URL that accepts `POST` with a JSON body. Any `#fragment` on the URL is dropped before sending.

### 

[​

](#what-gets-sent)

What gets sent

Each event is a single `POST` with `Content-Type: application/json`. There are **no other custom headers**: no signature, no timestamp, no event id. fileAI logs your response status but does not act on it, and sets no client timeout of its own. A typical successful upload sends events for these steps, in roughly this order: `processing_started` usually arrives twice: first with `waiting`, then with `completed`. Not every upload passes through every step, and the order is not guaranteed: key your logic on `step` and `status`, not on position. `fileIds` first appears on `split_classify`, and again on `processing_finished`. An upload that fails sends `processing_failed` with status `error` and an `errorReason`.

Reprocessing a document (for example, replacing its file) can send another round of events for the same `uploadId`, including `beethoven_ocr` or `ai_form_filling` with status `processing` and a further `processing_finished`. Make your handler safe to run more than once.

## 

[​

](#handle-callbacks-safely)

Handle callbacks safely

Upload callbacks are not signed, so anyone who learns your callback URL can send a request that looks like one. Treat a callback as a **signal to check**, not as proof:

1

Put a token in the callback URL

Add an unguessable value to the query string of each upload’s `callbackURL`, for example `https://example.com/fileai/callback?token=8f3c…`, and reject requests without it. fileAI keeps the query string and drops only a `#fragment`. This stops requests from anyone who doesn’t know the URL; it is not a signature.

2

Confirm the state with the API

Before anything consequential (paying, approving, releasing a claim), fetch the upload with your API key: `GET /v1/uploads/{uploadId}`. Act on the `status` of each document in `files` (for example `processed`), not on the callback body. See [Uploads in Async jobs](/api/async-jobs#uploads).

3

Make the handler safe to repeat

The same `uploadId` can send `processing_finished` more than once (see reprocessing above). Record which `uploadId` and `fileId`s you have already handled and skip repeats.

4

Keep a polling fallback

Callbacks are not retried. For any upload that hasn’t reached `processing_finished` in the time you expect, poll `GET /v1/uploads/{uploadId}`, or pull changes on a schedule with [incremental loading](/deliver/incremental-loading).

## 

[​

](#example)

Example

A successful upload that produced two documents ends with:

```
{
  "step": "processing_finished",
  "status": "completed",
  "uploadId": "e9818d17-e5be-4d57-bdc8-5a40b4f6f4e1",
  "fileIds": "b765bd00-181e-4737-b4d9-004dadd6bd45,3c1a9f02-77e4-4b1e-9a51-2f0d6c8e4a10"
}
```

A failed upload sends:

```
{
  "step": "processing_failed",
  "status": "error",
  "uploadId": "e9818d17-e5be-4d57-bdc8-5a40b4f6f4e1",
  "errorReason": "ocr_failed"
}
```

A minimal receiver checks the token, acknowledges fast, and hands the upload to a worker. The worker calls `GET /v1/uploads/{uploadId}` and uses only documents whose `status` is `processed`. `fileIds` is one comma-joined string, not an array, so split it before you record which documents you have handled.

Python

Node.js

```
import hmac, os
from flask import Flask, abort, request

app = Flask(__name__)

@app.post("/fileai/callback")
def fileai_callback():
    if not hmac.compare_digest(request.args.get("token", ""), os.environ["CALLBACK_TOKEN"]):
        abort(401)
    event = request.get_json()
    if event["step"] == "processing_finished" and event["status"] == "completed":
        file_ids = event.get("fileIds", "").split(",")  # a string, not an array
        enqueue_confirm_upload(event["uploadId"], file_ids)
    elif event["status"] == "error":
        enqueue_confirm_upload(event["uploadId"], [])
    return "", 200
```

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

const app = express();
app.use(express.json());

const expected = Buffer.from(process.env.CALLBACK_TOKEN);

app.post("/fileai/callback", (req, res) => {
  const token = Buffer.from(String(req.query.token ?? ""));
  if (token.length !== expected.length || !crypto.timingSafeEqual(token, expected)) {
    return res.sendStatus(401);
  }
  const { step, status, uploadId, fileIds } = req.body;
  if (step === "processing_finished" && status === "completed") {
    enqueueConfirmUpload(uploadId, (fileIds ?? "").split(",")); // a string, not an array
  } else if (status === "error") {
    enqueueConfirmUpload(uploadId, []);
  }
  res.sendStatus(200);
});
```

## 

[​

](#reference)

Reference

### 

[​

](#payload-fields)

Payload fields

Field

Type

Present

Description

`step`

string

Always

The processing step this event is about. See below.

`status`

string

Always

`waiting`, `processing`, `completed` or `error`.

`uploadId`

string

Always

The `uploadId` returned when you created the upload.

`fileIds`

string

`split_classify` and `processing_finished`

The `fileId`s of the documents produced from the upload, **joined with commas into one string**. Split it yourself.

`errorReason`

string

Only when there is a reason

Machine-readable failure reason, such as `ocr_failed`, `password_protected` or `corrupted_file`.

There is no `timestamp` field. Record the time you received the event if you need one; `GET /v1/uploads/{uploadId}` returns a `timestamp` per step.

### 

[​

](#statuses)

Statuses

`status`

Meaning

`waiting`

The step is queued. Sent for `processing_started`.

`processing`

The step is running. Sent when a document is reprocessed.

`completed`

The step finished successfully.

`error`

The step failed. On `split_classify`, it means the upload needs attention; check `errorReason`.

### 

[​

](#steps)

Steps

`step`

Sent when

`file_uploaded`

The file has landed in storage.

`processing_started`

The upload enters the processing queue (`waiting`), then starts (`completed`).

`fraud_detection`

An internal processing step has finished. This is not [fileForensics](/forensics/overview).

`first_page_analysis`

First-page analysis has finished.

`beethoven_ocr`

OCR has finished.

`ocr_evaluation`

OCR quality checks have finished.

`split_classify`

The upload has been split into documents and classified. First event carrying `fileIds`.

`ai_form_filling`

A reprocessed document’s values are being extracted again.

`processing_finished`

All documents are processed. Carries `fileIds`.

`processing_failed`

Processing stopped with an error. Carries `errorReason` when one is known.

## 

[​

](#related)

Related

## Async jobs

Upload status polling, and callbacks for extract, split, classify and agent jobs.

## Uploads

Single, batch and multipart uploads, where you set `callbackURL`.

## Incremental loading

Pull new and changed files on a schedule instead of reacting to events.

## API quickstart

Upload a file and read its values end to end.

[Previous](/extract/split-classify)[

Sync changes

Sync new and changed files to a data warehouse with timestamp filters on GET /v1/files.

Next

](/deliver/incremental-loading)

⌘I

## On this page

Back to top

*   [How it works](#how-it-works)
    *   [Where the callback goes](#where-the-callback-goes)
    *   [What gets sent](#what-gets-sent)
*   [Handle callbacks safely](#handle-callbacks-safely)
*   [Example](#example)
*   [Reference](#reference)
    *   [Payload fields](#payload-fields)
    *   [Statuses](#statuses)
    *   [Steps](#steps)
*   [Related](#related)
