# Uploads
Source: https://docs.file.ai/ingest/uploads

> Send files to fileAI one at a time, in batches of up to 100, in parts for large files, or as ZIP archives.

fileScout

# Uploads

Copy pageCopy page

Send files to fileAI one at a time, in batches of up to 100, in parts for large files, or as ZIP archives.

Copy pageCopy page

## 

[​

](#what-it-does)

What it does

Uploading is how files enter fileAI for processing. You ask for a presigned storage URL, `PUT` the file to it, and processing starts as soon as the file lands.

## 

[​

](#when-to-use-it)

When to use it

*   You receive invoices, statements or forms and want fileAI to classify them and extract values into your project.
*   You need to send many files at once (up to 100 per request), or files too large for one request (multipart, typically over 100 MB).
*   You have a folder of documents and want to send it as one `.zip` that fileAI unpacks and processes file by file.

Need values from one document without storing it? `POST /v1/extract` takes the file directly as `multipart/form-data` (up to 50 MB) and returns a job. See [Extract, split and classify](/extract/split-classify).

## 

[​

](#how-it-works)

How it works

You never send file bytes to the API itself. You track the upload with `GET /v1/uploads/{uploadId}` or a [callback](/deliver/callbacks); [Async jobs](/api/async-jobs) compares the two.

## 

[​

](#try-it)

Try it

Request an upload URL, then `PUT` the file to it. The URL is valid for **300 seconds** (5 minutes) and can be used more than once within that time. Send the same `Content-Type` you gave as `fileType` (the file’s MIME type). The cURL examples assume `FILEAI_BASE_URL` is set as in [Authentication](/authentication#switching-between-instances).

cURL

Python

Node.js

```
# 1. Request an upload URL
curl -X POST "$FILEAI_BASE_URL/files/upload" \
  -H "x-api-key: $FILEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fileName": "invoice.pdf",
    "fileType": "application/pdf",
    "callbackURL": "https://example.com/fileai/callback"
  }'

# 2. PUT the file to the presignedUploadURL from the response
curl -X PUT "<presignedUploadURL>" \
  -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf
```

```
import os, requests

BASE = "https://api.orion.file.ai/prod/v1"
HEADERS = {"x-api-key": os.environ["FILEAI_API_KEY"]}

upload = requests.post(
    f"{BASE}/files/upload",
    headers=HEADERS,
    json={
        "fileName": "invoice.pdf",
        "fileType": "application/pdf",
        "callbackURL": "https://example.com/fileai/callback",
    },
).json()

with open("invoice.pdf", "rb") as f:
    requests.put(
        upload["presignedUploadURL"],
        headers={"Content-Type": "application/pdf"},
        data=f,
    ).raise_for_status()
```

```
import { readFile } from "node:fs/promises";

const BASE = "https://api.orion.file.ai/prod/v1";
const headers = { "x-api-key": process.env.FILEAI_API_KEY };

const upload = await fetch(`${BASE}/files/upload`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    fileName: "invoice.pdf",
    fileType: "application/pdf",
    callbackURL: "https://example.com/fileai/callback",
  }),
}).then((r) => r.json());

await fetch(upload.presignedUploadURL, {
  method: "PUT",
  headers: { "Content-Type": "application/pdf" },
  body: await readFile("invoice.pdf"),
});
```

## 

[​

](#understand-the-result)

Understand the result

The upload request returns `201`:

Response (201)

```
{
  "uploadId": "f2538513-f0b9-4aa8-9c57-bc0a85c77de6",
  "presignedUploadURL": "https://…",
  "s3Path": "s3://…",
  "callbackURL": "https://example.com/fileai/callback",
  "callbackURLSource": "user",
  "isSplit": false,
  "isSplitExcel": false
}
```

Field

Meaning

`uploadId`

The id you track with `GET /v1/uploads/{uploadId}`.

`presignedUploadURL`

Where you `PUT` the file bytes. Valid for 300 seconds.

`callbackURLSource`

Which callback URL applies: `user` (the one you sent) or `api_key` (the key’s default).

Once the file lands, poll the upload, or wait for the request to your `callbackURL`:

```
curl "$FILEAI_BASE_URL/uploads/<uploadId>" -H "x-api-key: $FILEAI_API_KEY"
```

Response (200)

```
{
  "fileType": "application/pdf",
  "statuses": [
    { "step": "processing_started", "status": "completed", "timestamp": "2025-06-11T10:30:30Z" },
    { "step": "fraud_detection", "status": "completed", "timestamp": "2025-06-11T10:31:00Z" }
  ],
  "files": [
    { "fileId": "53d6a0b1-…", "fileName": "invoice.pdf", "status": "processed", "fileClass": "Invoice", "schemaId": "6835aca6…" }
  ]
}
```

Field

Meaning

`statuses[]`

Processing steps in order, each with `step`, `status`, `timestamp`, and `errorReason` when a step fails. Step and status values are listed in [Async jobs](/api/async-jobs).

`files[]`

The **documents** created from the upload. One file can become several documents when it is split. Use each `fileId` with `GET /v1/files/{fileId}/values`.

The `fraud_detection` step is an internal processing step, not [fileForensics](/forensics/overview). To check a file with fileForensics, see [Choose products](#choose-products).

## 

[​

](#next-steps)

Next steps

## Processing callbacks

Get notified instead of polling.

## Supported file types

Every type fileAI accepts.

## Files and folders

Organize uploads into folders.

## Idempotency

Retry uploads safely.

## 

[​

](#reference)

Reference

### 

[​

](#upload-routes)

Upload routes

You have

Use

Limit per request

One file

`POST /v1/files/upload`

1 file

Many files

`POST /v1/files/upload/batch`

100 files

One large file

`POST /v1/files/upload/multipart`, then `/multipart/complete`

1 file

Many large files

`POST /v1/files/upload/multipart/batch`, then `/multipart/complete/batch`

25 files, 2,500 part URLs in total

A folder of files in one archive

Any route above with a `.zip` file

See [ZIP uploads](#zip-uploads)

### 

[​

](#batch-registration)

Batch registration

`POST /v1/files/upload/batch` registers up to 100 files in one call and returns a presigned URL for each. Put shared options in `defaults`; any field on a file overrides the default.

```
curl -X POST "$FILEAI_BASE_URL/files/upload/batch" \
  -H "x-api-key: $FILEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1d2c3e-1b7a-4b8e-9a51-2f0c4d1e9b77" \
  -d '{
    "defaults": { "directoryId": "649e2d2d2d2d2d2d2d2d2d2d", "callbackURL": "https://example.com/fileai/callback" },
    "files": [
      { "fileName": "inv-001.pdf", "fileType": "application/pdf" },
      { "fileName": "inv-002.pdf", "fileType": "application/pdf", "pageCount": 3 },
      { "fileName": "scan.png", "fileType": "image/png", "ocrModel": "Not_A_Model" }
    ]
  }'
```

Response (207)

```
{
  "batchId": "k3x9q2m7v1c8z4n6b0w5t",
  "apiRequestId": "k3x9q2m7v1c8z4n6b0w5t",
  "accepted": 2,
  "rejected": 1,
  "results": [
    { "index": 0, "status": "accepted", "fileName": "inv-001.pdf", "uploadId": "…", "presignedUploadURL": "https://…", "expiresAt": "…" },
    { "index": 1, "status": "accepted", "fileName": "inv-002.pdf", "uploadId": "…", "presignedUploadURL": "https://…", "expiresAt": "…" },
    { "index": 2, "status": "rejected", "fileName": "scan.png", "error": { "status": 422, "code": "VALIDATION_FAILED", "detail": "Invalid ocr model" } }
  ]
}
```

*   `results[i]` always maps to `files[i]`. One bad file never rejects the others.
*   `batchId` is a 21-character lowercase id with no prefix. It is also the `apiRequestId` unless you set one in `defaults`.
*   The status is `201` when every file is accepted and `207` when the result is mixed.
*   An empty or missing `files` array returns `422` `INVALID_BATCH`; more than 100 files returns `422` `BATCH_TOO_LARGE`. Nothing is registered in either case.
*   A file over the page limit (when you send `pageCount`) is rejected on its own.
*   Send an `Idempotency-Key` (1–255 characters) to make the call safe to retry. A replay returns the same `batchId`, `uploadId`s and verdicts, with freshly signed URLs. See [Idempotency](/api/idempotency).
*   `isEphemeral` and `apiRequestId` apply to the whole batch on this route, so they are only accepted in `defaults`.

Each URL is valid for 300 seconds. `PUT` each file to its own URL as in the single upload.

### 

[​

](#multipart-uploads-for-large-files)

Multipart uploads for large files

Multipart uploads split one file into parts that you upload in parallel and then stitch together. Use them for large files (typically over 100 MB).

1

Initiate

Send the file size in MB. `partSizeLimit` (MB) is optional; parts are at least 5 MB, and the file is cut into at most 10,000 parts.

```
curl -X POST "$FILEAI_BASE_URL/files/upload/multipart" \
  -H "x-api-key: $FILEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "fileName": "ledger.pdf", "fileType": "application/pdf", "fileSize": 150.5, "partSizeLimit": 10 }'
```

Response (201)

```
{
  "id": "upload_aws_xyz789",
  "key": "…/ledger.pdf",
  "uploadId": "file_abc123xyz",
  "partSize": 10485760,
  "totalParts": 16,
  "presignedUrls": [
    { "partNumber": 1, "presignedUrl": "https://…", "startByte": 0, "endByte": 10485759, "size": 10485760 }
  ]
}
```

Keep `id` and `key` to complete the upload. `uploadId` is the id you track with `GET /v1/uploads/{uploadId}`.

2

Upload each part

`PUT` bytes `startByte`–`endByte` to each part’s `presignedUrl`, and save the `ETag` header from every response. Part URLs are valid for 900 seconds (15 minutes).

3

Complete

```
curl -X POST "$FILEAI_BASE_URL/files/upload/multipart/complete" \
  -H "x-api-key: $FILEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "upload_aws_xyz789",
    "key": "…/ledger.pdf",
    "parts": [ { "partNumber": 1, "eTag": "\"abc123\"" }, { "partNumber": 2, "eTag": "\"def456\"" } ]
  }'
```

The response includes `success: true`, and processing starts.

#### 

[​

](#many-large-files)

Many large files

*   **Initiate:** `POST /v1/files/upload/multipart/batch` takes up to 25 files, with at most 2,500 part URLs across the batch. Over that, the request returns `422` `BATCH_PARTS_TOO_LARGE` and nothing is created; split the batch or raise `partSizeLimit`. Each file is accepted or rejected on its own, and the response is `201` even if every file is rejected, so check `accepted`, `rejected` and each result’s `status`.
*   **Field names differ here:** in batch results, `uploadId` is the storage multipart id (send it as `id` to complete or refresh) and `fileId` is the fileAI id you track with `GET /v1/uploads/{uploadId}`. Accepted results also include `key` and `s3Path`.
*   **Per-file options:** unlike `/files/upload/batch`, this route accepts `isEphemeral` on each file as well as in `defaults`.
*   **Complete:** `POST /v1/files/upload/multipart/complete/batch` with `uploads` (each `id`, `key` and full `parts` list) and an optional `batchId`. Each upload completes on its own: `201` when all succeed, `207` when some fail. Retry only the failed ones. A bad part list fails with `INVALID_PARTS`.
*   **Refresh URLs:** a long transfer can outlive its part URLs. `POST /v1/files/upload/multipart/urls/refresh` with `id`, `key` and the `partNumbers` still outstanding returns new URLs valid for another 900 seconds. Parts already uploaded stay uploaded. An unknown `id`/`key` returns `404`.

`Idempotency-Key` is ignored on `/multipart/batch` and `/multipart/urls/refresh`: a retry starts a fresh set of uploads or signs fresh URLs. `/multipart/complete/batch` is idempotent for 24 hours. Abandoned multipart uploads expire on their own.

### 

[​

](#zip-uploads)

ZIP uploads

Upload a `.zip` through any route above. fileAI validates the archive, extracts it, and processes each supported file inside as its own upload. Set `retainOriginalZipFileName: true` to keep the archive’s name on the extracted files as `zipArchiveName`. Files from one ZIP share an `apiRequestId` (the ZIP’s id unless you supply one). `GET /v1/uploads/{uploadId}` for a ZIP returns one entry per extracted file in `files`, each with its own `uploadId`, `statuses` and `files` (documents). Files that could not be processed carry a `processing_failed` step with status `error` and an `errorReason` such as `Unsupported file type`. When some files succeed and some fail, the response is `207` instead of `200`. If the archive itself fails validation, you get a `zip_validation` step with status `error` and an empty `files` array.

### 

[​

](#upload-options)

Upload options

These body fields are accepted on `POST /v1/files/upload`, and on the batch and multipart routes unless noted.

Field

Type

What it does

`fileName`

string, required

File name, including extension.

`fileType`

string, required

The file’s **MIME type**, such as `application/pdf`. This is not the fileAI file type (see [Concepts](/concepts)). Accepted values: [Supported file types](/ingest/supported-files).

`callbackURL`

string

URL fileAI calls once with processing updates (no retries). Use HTTPS. Overrides the key’s default callback URL; `callbackURLSource` in the response says which one applies (`user` or `api_key`). On `/files/upload/batch` the order is the file’s own URL, then `defaults`, then the key’s default. **Multipart routes do not fall back** to the key’s default: send `callbackURL` on each multipart upload that needs one. Payload: [Processing callbacks](/deliver/callbacks).

`directoryId`

string

Folder to place the file in. See [Files and folders](/ingest/folders).

`destinationPath`

string

Slash-delimited folder path such as `clients/acme`. Missing folders are created. Works with `directoryId`.

`blueprintId`

string

File type id from `GET /v1/file-type` (the API calls a file type a blueprint). Applies that file type and its schema to every document in the file and skips auto-classification. Must be approved (Verify in the app) or built in.

`ocrModel`

string

OCR model for this file, overriding the project default. See [Read and parse](/ingest/ocr-models).

`schemaLocking`

boolean

Only classify into your saved schemas. See [Schema locking](/extract/schema-locking).

`isSplit`

boolean

Split a file that holds several documents into separate documents. Default `false`.

`isSplitExcel`

boolean

Process each worksheet of an Excel file as its own document. Default `false`.

`isEphemeral`

boolean

Delete the file and all related data after it is processed. Default `false`. On `/files/upload/batch`, only in `defaults`; `/multipart/batch` also accepts it per file.

`pageCount`

number

Page count, checked early against your page limit.

`apiRequestId`

string

Your own id to group related uploads. In a batch, only in `defaults`.

`retainOriginalZipFileName`

boolean

Keep a ZIP’s original name on its extracted files. Default `false`.

`storageOnly`

boolean

Store the file without OCR, classification or extraction, for example to run [fileForensics](/forensics/overview) later. Single upload only. See [Choose products](#choose-products).

`enabledProducts`

array

`FILE_SCOUT`, `FILE_FORENSICS` or both. Single upload only. See [Choose products](#choose-products).

`platformIngest`

boolean

Convert JPEG and PNG uploads to PDF before processing, as the app does. Single upload only.

`fileSize`, `partSizeLimit`

number (MB)

Multipart routes only.

Validation errors return `422`, for example `Invalid fileName.`, `Invalid directoryId.` or an `ocrModel` that is retired or unknown.

### 

[​

](#choose-products)

Choose products

`enabledProducts` on `POST /v1/files/upload` picks which products run on the file and are metered. It is accepted **on single uploads only**, and only in workspaces enabled for these products; otherwise the request returns `422`.

`enabledProducts`

`storageOnly`

What happens

Left out

Either

The upload follows your workspace’s existing processing and billing.

`["FILE_SCOUT"]`

Must be `false` or left out

Full processing: OCR, classification and extraction. See [fileScout](/scout/overview).

`["FILE_FORENSICS"]`

**Must be `true`**

The file is stored and checked with [fileForensics](/forensics/overview) only.

`["FILE_SCOUT", "FILE_FORENSICS"]`

Must be `false` or left out

Full processing plus fileForensics.

A storage-only upload without `enabledProducts` can later be sent to `POST /v1/forensics`.

### 

[​

](#size-limits)

Size limits

The maximum size for presigned and multipart uploads is set per instance. A file over it is marked as exceeding the maximum file size and is not processed. `POST /v1/extract`, `/split` and `/classify` accept files up to 50 MB.

[Previous](/scout/overview)[

File types

Every file format fileAI accepts for upload, the size limits, and how to prepare files for the best results.

Next

](/ingest/supported-files)

⌘I

## On this page

*   [What it does](#what-it-does)
*   [When to use it](#when-to-use-it)
*   [How it works](#how-it-works)
*   [Try it](#try-it)
*   [Understand the result](#understand-the-result)
*   [Next steps](#next-steps)
*   [Reference](#reference)
    *   [Upload routes](#upload-routes)
    *   [Batch registration](#batch-registration)
    *   [Multipart uploads for large files](#multipart-uploads-for-large-files)
    *   [Many large files](#many-large-files)
    *   [ZIP uploads](#zip-uploads)
    *   [Upload options](#upload-options)
    *   [Choose products](#choose-products)
    *   [Size limits](#size-limits)
