Inovacc Developer

API reference

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 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. 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).

  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). 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

LimitValue
File or blob in one request100 MiB, Content-Length required
File or blob uploaded in parts5 GiB; parts 5 MiB to 100 MiB (except the last), numbered 1 to 10,000
Path1,024 bytes
Listing prefix256 bytes
Metadata per file2 KiB
Listing page1 to 200 (default 50)
Shared folder name1 to 64 letters, digits, _ or -
Rate600 requests per minute per credential holder, per location

Pricing: on request.

Errors

StatusCodeWhat it means and what to do
400invalid_path, invalid_prefixThe path or prefix is not in canonical form; fix it on your side.
400invalid_metadataContent-Type or X-File-Meta-* breaks the rules or is over 2 KiB.
400hash_mismatchThe bytes do not match the SHA-256 you sent; nothing was stored.
400invalid_part, invalid_limit, invalid_cursor, invalid_granteeFix the upload parts, the listing or the grant.
401invalid_credentialsSend a valid credential.
403application_requiredPath routes need a credential bound to an application.
403forbiddenMissing permission, or a read grantee tried to write.
404file_not_found, blob_not_found, share_not_found, upload_not_foundIt does not exist, or you have no access to it.
409already_exists, not_emptyThe shared folder name is taken; empty the folder before deleting it.
411length_requiredSend Content-Length.
413payload_too_largeOver 100 MiB in one request (use parts) or over 5 GiB.
429rate_limitedSlow down.
503service_unavailableRetry 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.

Authentication

Every call carries your key; your organization comes from it. See the authentication guide.

HeaderAuthorizationBearer <API key>

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

Language

⋮
PUT /v1/databases/{database}/blobs/{sha256}Example

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 '{}'

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());

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())

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))
}

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(())
}

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());

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);

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}"

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());
    }
}

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()}");

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()}")
}

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))
Source details

Catalogue entry

Id
identity/component-taxonomy/capabilities#files
Repository
identity
Path
contracts/component-taxonomy/catalog/capabilities.json
Commit
08ef3cd75d82

Example

Id
identity/component-taxonomy/quickstart#files
Repository
identity
Path
contracts/component-taxonomy/catalog/capabilities.json
Commit
08ef3cd75d82
Updated 2026-10-10.