Files
Content-addressed file storage, with multipart upload for large files.
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 can point at it with ablobfield.
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. To make a file searchable by meaning, add it to a collection of Knowledge and vector search.
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 etags. 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 with the file's hash and size.
Get started
You need a secret key bound to an application, with blob permissions (Authentication).
- Upload a file.
PUT /v1/files/reports/2026-10.csvwith the file as the body andContent-Type: text/csv(see the samples). The answer is 201 with itssha256. - Read it back.
GET /v1/files/reports/2026-10.csv. The bytes arrive withETagequal to the hash from step 1. - List the folder.
GET /v1/files?prefix=reports/&delimiter=/. Your file is initems. - Replace it.
PUTthe same path with new bytes. The answer is 200, with a newsha256. - Delete it.
DELETE /v1/files/reports/2026-10.csvanswers 204; a secondGETis 404file_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-Sha256on every write so a damaged upload is refused instead of stored. - Use parts above 100 MiB, and start with
POST .../uploads: it answersexists: truewhen 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_cursorto the end when listing; short pages are normal. - Grant
readunless 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: records that point at files with
blobfields - Activity log: uploads and downloads with their hashes
- Knowledge and vector search: make documents searchable
- Authentication, Errors, Limits
Authentication
Every call carries your key; your organization comes from it. See the authentication guide.
AuthorizationEndpoints
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
Language
⋮
cURL
# The body is {} here: see parameters.
curl -X PUT "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}" \
-H "Authorization: Bearer $INOVACC_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'# The body is {} here: see parameters.
curl -X PUT "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}" \
-H "Authorization: Bearer $INOVACC_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
TypeScript
// The body is {} here: see parameters.
const body: Record<string, unknown> = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());// The body is {} here: see parameters.
const body: Record<string, unknown> = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());
Python
import json
import os
import urllib.request
# The body is {} here: see parameters.
body = {}
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}",
data=json.dumps(body).encode(),
method="PUT",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())import json
import os
import urllib.request
# The body is {} here: see parameters.
body = {}
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}",
data=json.dumps(body).encode(),
method="PUT",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"
// The body is {} here: see parameters.
const body = `{}`
func main() {
req, err := http.NewRequest("PUT", url, strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"
// The body is {} here: see parameters.
const body = `{}`
func main() {
req, err := http.NewRequest("PUT", url, strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}
Rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// Cargo.toml: serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
// The body is {} here: see parameters.
let body = serde_json::json!({});
let response = reqwest::blocking::Client::new()
.put("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")
.bearer_auth(api_key)
.json(&body)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// Cargo.toml: serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
// The body is {} here: see parameters.
let body = serde_json::json!({});
let response = reqwest::blocking::Client::new()
.put("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")
.bearer_auth(api_key)
.json(&body)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}
JavaScript
// The body is {} here: see parameters.
const body = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());// The body is {} here: see parameters.
const body = {};
const url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
console.log(response.status, await response.text());
PHP
<?php
// The body is {} here: see parameters.
$body = <<<'JSON'
{}
JSON;
$curl = curl_init('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}');
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);<?php
// The body is {} here: see parameters.
$body = <<<'JSON'
{}
JSON;
$curl = curl_init('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}');
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);
Ruby
require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}')
# The body is {} here: see parameters.
body = <<~'JSON'
{}
JSON
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = body
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}')
# The body is {} here: see parameters.
body = <<~'JSON'
{}
JSON
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = body
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
// The body is {} here: see parameters.
String body = "{}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
// The body is {} here: see parameters.
String body = "{}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
C#
using System.Text;
// The body is {} here: see parameters.
var body = "{}";
var url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");using System.Text;
// The body is {} here: see parameters.
var body = "{}";
var url = "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Put, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
Kotlin
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
// The body is {} here: see parameters.
val body = "{}"
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
// The body is {} here: see parameters.
val body = "{}"
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.method("PUT", HttpRequest.BodyPublishers.ofString(body))
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}
Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// The body is {} here: see parameters.
let body = #"""
{}
"""#
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(body.utf8)
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
// The body is {} here: see parameters.
let body = #"""
{}
"""#
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases/{database}/blobs/{sha256}")!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data(body.utf8)
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))
Source details
- Id
- identity/component-taxonomy/capabilities#files
- Repository
- identity
- Path
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82
- Id
- identity/component-taxonomy/quickstart#files
- Repository
- identity
- Path
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82