# Files

> Upload images and documents to Fileverity: multipart for files up to 4 MB, pre-signed upload for files up to 50 MB. File object, limits and retention.

Source: https://fileverity.com/en/docs/files

Images and documents are checked in two steps. First you upload the file with `POST /files` and receive a file ID. Then you create an [analysis](https://fileverity.com/en/docs/analyses.md) that references that ID. Text does not need a file; use [Text detections](https://fileverity.com/en/docs/text-detections.md).

## Supported files

|  | Images (fvimg-2) | Documents (fvdoc-3) |
| --- | --- | --- |
| Formats | `jpeg`, `png`, `webp` | `pdf`, `docx`, `doc`, `rtf`, `odt`, `pptx`, `txt` |
| Maximum size | 10 MB | 50 MB |
| Other limits | 40 megapixels | 100 pages |

## Two ways to upload

| Method | Request | Size | File status in the response |
| --- | --- | --- | --- |
| Multipart | `multipart/form-data` with the field `file` | Up to 4 MB | `uploaded` |
| Pre-signed URL | JSON with `filename`, `content_type` and `bytes`, then a `PUT` of the bytes | Up to 50 MB | `pending` until the bytes arrive |

Images are limited to the image size above whichever method you use.

## Multipart upload

Send files up to 4 MB as `multipart/form-data` in the field `file`. The format is detected from the file contents, not from its name or declared type.

**Multipart upload: cURL**

```bash
curl https://api.fileverity.com/v1/files \
  -H "Authorization: Bearer $FILEVERITY_API_KEY" \
  -F file=@claim-photo.jpg
```

**Multipart upload: JavaScript**

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

const form = new FormData();
form.append("file", new Blob([await readFile("claim-photo.jpg")]), "claim-photo.jpg");
const response = await fetch("https://api.fileverity.com/v1/files", {
  method: "POST",
  headers: { Authorization: "Bearer " + process.env.FILEVERITY_API_KEY },
  body: form,
});
const file = await response.json();
```

**Multipart upload: Python**

```python
import os
import requests

response = requests.post(
    "https://api.fileverity.com/v1/files",
    headers={"Authorization": "Bearer " + os.environ["FILEVERITY_API_KEY"]},
    files={"file": open("claim-photo.jpg", "rb")},
)
file = response.json()
```

**201 Created**

```json
{
  "id": "file_8Hk2pQ4vN9sL1xR7cW3mY6zB",
  "object": "file",
  "filename": "claim-photo.jpg",
  "bytes": 2481920,
  "content_type": "image/jpeg",
  "status": "uploaded",
  "livemode": true,
  "created": 1793700000,
  "expires_at": 1793786400
}
```

## Pre-signed upload

For files larger than 4 MB, up to 50 MB, ask for an upload URL, then send the bytes straight to storage.

1. `POST /files` with a JSON body: `filename` (optional), `content_type` and the exact size in `bytes`. The response has `status: pending` and an `upload` object.
2. `PUT` the file bytes to `upload.url` with the headers in `upload.headers`, before `upload.expires_at`. No `Authorization` header is needed for this request.
3. Optionally poll `GET /files/{id}` until `status` is `uploaded`.
4. Create the analysis with the file ID.

**Pre-signed upload: cURL**

```bash
FILE=$(curl -s https://api.fileverity.com/v1/files \
  -H "Authorization: Bearer $FILEVERITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "contract.pdf", "content_type": "application/pdf", "bytes": 18874368}')

UPLOAD_URL=$(echo "$FILE" | jq -r .upload.url)
FILE_ID=$(echo "$FILE" | jq -r .id)

curl -X PUT "$UPLOAD_URL" \
  -H "content-type: application/pdf" \
  --data-binary @contract.pdf
```

**Pre-signed upload: JavaScript**

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

const bytes = await readFile("contract.pdf");
const response = await fetch("https://api.fileverity.com/v1/files", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.FILEVERITY_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ filename: "contract.pdf", content_type: "application/pdf", bytes: bytes.length }),
});
const file = await response.json();

await fetch(file.upload.url, { method: "PUT", headers: file.upload.headers, body: bytes });
```

**Pre-signed upload: Python**

```python
import os
import requests

data = open("contract.pdf", "rb").read()
response = requests.post(
    "https://api.fileverity.com/v1/files",
    headers={"Authorization": "Bearer " + os.environ["FILEVERITY_API_KEY"]},
    json={"filename": "contract.pdf", "content_type": "application/pdf", "bytes": len(data)},
)
file = response.json()

requests.put(file["upload"]["url"], headers=file["upload"]["headers"], data=data)
```

**201 Created**

```json
{
  "id": "file_Pw3mY6zB8Hk2pQ4vN9sL1xR7",
  "object": "file",
  "filename": "contract.pdf",
  "bytes": 18874368,
  "content_type": "application/pdf",
  "status": "pending",
  "livemode": true,
  "created": 1793700000,
  "expires_at": 1793786400,
  "upload": {
    "url": "https://example.private.blob.vercel-storage.com/f/0d6f1c2e-7b1a-4c53-9a0e-5e8a41b3c7d2?signature=...",
    "method": "PUT",
    "headers": {
      "content-type": "application/pdf"
    },
    "expires_at": 1793703600
  }
}
```

The URL shown is shortened. Use the one in your response as it is. Send the `content-type` header exactly as returned in `upload.headers`.

### Accepted content types

| Kind | Content types |
| --- | --- |
| Images | `image/jpeg`, `image/png`, `image/webp` |
| Documents | `application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/msword`, `application/rtf`, `application/vnd.oasis.opendocument.text`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `text/plain` |

## The file object

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | File ID, prefixed `file_`. |
| `object` | string | Always `file`. |
| `filename` | string or null | Sanitized file name. |
| `bytes` | integer or null | Size in bytes. |
| `content_type` | string | Detected or declared content type. |
| `status` | string | `pending` until uploaded bytes have arrived, then `uploaded`. |
| `livemode` | boolean | `false` for files created with a test key. Files are separated by mode. |
| `created` | integer | Unix timestamp. |
| `expires_at` | integer | Unix timestamp after which the file is deleted. |
| `upload` | object | Only on the response to a JSON upload request: `url`, `method` (`PUT`), `headers` and `expires_at`. |

## Retention

- A file is kept for at most 24 hours (see `expires_at`) and then deleted.
- A file is deleted as soon as an analysis starts from it. Each uploaded file is used by one analysis; upload it again to check it again.
- `DELETE /files/{id}` removes a file and its stored bytes earlier.
- A workspace can hold up to 50 unexpired files and 200 MB per mode at once. Beyond that, uploads fail with `pending_files_limit` (HTTP 429, no `retry-after`) until files are used, deleted or expire.
- A file ID that is unknown, expired or already used returns `file_not_found` when you create an analysis, and `not_found` when you retrieve or delete it. `file_not_found` is also returned when the upload to `upload.url` has not finished yet, so wait until `GET /files/{id}` shows `uploaded`.

## Errors

| Code | Status | Cause |
| --- | --- | --- |
| `missing_parameter` | 400 | No `file` field in the multipart body. |
| `unsupported_media_type` | 415 | The body is neither `multipart/form-data` nor JSON. |
| `unsupported_file` | 415 | The format is not supported. |
| `invalid_file` | 422 | The file is empty, damaged or cannot be read. |
| `file_too_large` | 413 | A multipart file larger than 4 MB, or `bytes` above 50 MB. |
| `invalid_parameter` | 400 | A JSON field is missing, mistyped or unknown. |
| `invalid_json` | 400 | The JSON body is not an object. |
| `pending_files_limit` | 429 | Too many unexpired uploads in the workspace. Not sent with a `retry-after` header. |
| `idempotency_mismatch`, `idempotency_in_progress` | 409 | The `Idempotency-Key` was used with a different request, or its first request is still running. |

> **Note.** Test keys validate uploads but do not store the bytes. A JSON upload request with a test key returns a test upload object (`upload.test` is `true`, the file is already `uploaded`): there is nothing to PUT.

