# Files

Content-addressed file storage, with multipart upload for large files.

Base URL: `https://data.inovacc.dev`

## Overview

Files stores your application's binary content (images, documents, exports, recordings) on `https://data.inovacc.dev`, with uploads in parts for anything large. It offers two ways to address a file:

- **by path, in your application's folder**: `PUT /v1/files/users/123/avatar.png`, the way a cloud storage bucket names files, with listings by folder, user metadata and folders shared with other applications of your workspace;
- **by content, inside a database**: `PUT /v1/databases/{db}/blobs/{sha256}`, where the file's own SHA-256 is its name, so a record of [Data](/en/products/data/) can point at it with a `blob` field.

The problem it solves is keeping files next to your data without running storage: Inovacc chooses where bytes live, verifies them against their hash, removes duplicates inside your folder, and serves them back with headers that stop a downloaded file from running in a browser.

Use Files for anything that is bytes rather than fields. Keep the structured description of a file (owner, title, status) as a record in [Data](/en/products/data/). To make a file searchable by meaning, add it to a collection of [Knowledge and vector search](/en/products/knowledge/).

## Concepts

**Your application's folder.** Every application has its own folder. The folder is chosen from the credential, never from the request: a credential bound to an application uses that application's folder, and a credential bound to no application gets `403 application_required` on every path route. Paths are UTF-8, 1 to 1,024 bytes, separated by `/`, case-sensitive, with no empty, `.` or `..` piece; a path that is not already in that form is refused, never silently rewritten.

**Metadata.** A write may carry a `Content-Type` and your own `X-File-Meta-<name>` headers (2 KiB in total). Both come back on every read. A write replaces the file's content, type and metadata whole.

**Hashes.** Every file is identified by its SHA-256. Send `X-File-Sha256` with a write and the bytes are checked against it (`400 hash_mismatch`, nothing stored). Inside one application's folder, two paths with the same bytes share one stored copy; the copy is kept until the last path goes. Deduplication never crosses applications or organizations.

**Database blobs.** A blob is addressed by the SHA-256 in its path inside one database. Uploading it again answers `200` instead of `201`. Anyone with read permission on the database who knows the hash can read it: record rules do not apply to blobs.

**Shared folders.** An application can create a folder `team` that appears to every application with access as `shared/team/...`. The creator owns it and grants other applications of the same workspace `read` or `read_write`. An application without access cannot tell the folder exists: every operation answers `404`.

**Large files.** Up to 100 MiB goes in one request. Above that, up to 5 GiB, you upload in parts and then link the result to a path (or it becomes the blob).

## How it works

Every call carries `Authorization: Bearer <credential>`; there is no organization header, because your organization and application come from the credential. The permission checked is blob read, write or delete. A web page can use these routes with a publishable key from an origin its application lists.

**Writing by path.** `PUT /v1/files/{path}` with the bytes and a `Content-Length` (required). The answer is `201` for a new path or `200` for a replacement: `{"path","size","sha256","content_type","updated_at","metadata"}` and `ETag: "<sha256>"`.

**Reading.** `GET /v1/files/{path}` returns the whole file (ranges are not supported) with `Content-Type`, `ETag`, `X-File-Sha256`, `X-File-Updated-At` and your `X-File-Meta-*` headers. Every download also carries `X-Content-Type-Options: nosniff`, a `Content-Security-Policy` that sandboxes it and `Cache-Control: no-store`, so an uploaded HTML page can never run as your site. `HEAD` returns the headers alone.

**Listing.** `GET /v1/files?prefix=photos/&delimiter=/` lists the files directly under a folder as `items` and each sub-folder once in `prefixes`. Follow `next_cursor` until it is `null`: a page may be shorter than `limit` and still have a next one.

**In parts.** `POST /v1/files-uploads/{sha256}` starts an upload (or answers `200` with `exists: true` when your folder already holds those bytes). Send parts 1 to 10,000 with `PUT .../{upload_id}/parts/{n}`, each between 5 MiB and 100 MiB except the last, then `POST .../complete` with the list of parts and their `etag`s. Inovacc reads the whole object back and checks its SHA-256 and size. Finally `PUT /v1/files/{path}` with `X-File-Sha256` and an empty body gives it a path. Database blobs use the same four steps under `/v1/databases/{db}/blobs/{sha256}/uploads`.

Uploads, downloads and deletes are recorded in the [Activity log](/en/products/activity/) with the file's hash and size.

## Get started

You need a secret key bound to an application, with blob permissions ([Authentication](/en/guides/authentication/)).

1. **Upload a file.** `PUT /v1/files/reports/2026-10.csv` with the file as the body and `Content-Type: text/csv` (see [the samples](#example)). The answer is `201` with its `sha256`.
2. **Read it back.** `GET /v1/files/reports/2026-10.csv`. The bytes arrive with `ETag` equal to the hash from step 1.
3. **List the folder.** `GET /v1/files?prefix=reports/&delimiter=/`. Your file is in `items`.
4. **Replace it.** `PUT` the same path with new bytes. The answer is `200`, with a new `sha256`.
5. **Delete it.** `DELETE /v1/files/reports/2026-10.csv` answers `204`; a second `GET` is `404 file_not_found`.

## Use cases

**Profile pictures from the browser.** A web app uploads each avatar to `users/<id>/avatar.png` with a publishable key and keeps the returned `sha256` in the user's record, so a page can tell when the picture changed.

**Exports shared with a partner app.** A reporting application creates the shared folder `exports`, grants the billing application `read`, and writes monthly files under `shared/exports/`. The billing application lists and downloads them; revoking the grant takes effect on its very next request.

**Large media with integrity.** A video tool hashes a 2 GiB recording on the client, uploads it in 100 MiB parts and completes it. Inovacc stores it only if the SHA-256 matches, so a corrupted transfer never becomes a file.

## Limits and pricing

| Limit | Value |
|---|---|
| File or blob in one request | 100 MiB, `Content-Length` required |
| File or blob uploaded in parts | 5 GiB; parts 5 MiB to 100 MiB (except the last), numbered 1 to 10,000 |
| Path | 1,024 bytes |
| Listing prefix | 256 bytes |
| Metadata per file | 2 KiB |
| Listing page | 1 to 200 (default 50) |
| Shared folder name | 1 to 64 letters, digits, `_` or `-` |
| Rate | 600 requests per minute per credential holder, per location |

**Pricing:** on request.

## Errors

| Status | Code | What it means and what to do |
|---|---|---|
| 400 | `invalid_path`, `invalid_prefix` | The path or prefix is not in canonical form; fix it on your side. |
| 400 | `invalid_metadata` | `Content-Type` or `X-File-Meta-*` breaks the rules or is over 2 KiB. |
| 400 | `hash_mismatch` | The bytes do not match the SHA-256 you sent; nothing was stored. |
| 400 | `invalid_part`, `invalid_limit`, `invalid_cursor`, `invalid_grantee` | Fix the upload parts, the listing or the grant. |
| 401 | `invalid_credentials` | Send a valid credential. |
| 403 | `application_required` | Path routes need a credential bound to an application. |
| 403 | `forbidden` | Missing permission, or a `read` grantee tried to write. |
| 404 | `file_not_found`, `blob_not_found`, `share_not_found`, `upload_not_found` | It does not exist, or you have no access to it. |
| 409 | `already_exists`, `not_empty` | The shared folder name is taken; empty the folder before deleting it. |
| 411 | `length_required` | Send `Content-Length`. |
| 413 | `payload_too_large` | Over 100 MiB in one request (use parts) or over 5 GiB. |
| 429 | `rate_limited` | Slow down. |
| 503 | `service_unavailable` | Retry later. |

## Best practices

- **Send `X-File-Sha256` on every write** so a damaged upload is refused instead of stored.
- **Use parts above 100 MiB**, and start with `POST .../uploads`: it answers `exists: true` when the bytes are already there.
- **Store the `sha256`, not just the path**, in your records; it tells you exactly which content a record refers to.
- **Follow `next_cursor` to the end** when listing; short pages are normal.
- **Grant `read` unless a partner must write**, and revoke grants you no longer need.
- **Do not rely on blobs being private by rule**: anyone who may read the database and knows the hash can read a blob.

## Related

- [Data](/en/products/data/): records that point at files with `blob` fields
- [Activity log](/en/products/activity/): uploads and downloads with their hashes
- [Knowledge and vector search](/en/products/knowledge/): make documents searchable
- [Authentication](/en/guides/authentication/), [Errors](/en/guides/errors/), [Limits](/en/guides/limits/)

## Endpoints

- PUT /v1/databases/{database}/blobs/{sha256}
- GET /v1/databases/{database}/blobs/{sha256}
- DELETE /v1/databases/{database}/blobs/{sha256}
- POST /v1/databases/{database}/blobs/{sha256}/uploads

## Example

```sh
curl -X PUT "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}" -H "Authorization: Bearer $INOVACC_API_KEY" -H "Content-Type: application/json" -d '{}'
```
