Skip to content
Go To Dashboard

File Storage

Persist files your agents generate or fetch — images, documents, datasets — and retrieve or share them later. Bytes flow directly between your client and Google Cloud Storage via short-lived presigned URLs; Sapiom only mints the URLs and tracks metadata. No GCS account, no bucket setup.

Reach file storage from a step through the typed ctx.sapiom.fileStorage client (upload, getDownloadUrl, list, setVisibility, delete) — sapiom_dev_orchestrations_check validates the call at author time. See Using Capabilities.

import { createFetch } from "@sapiom/fetch";
const sapiomFetch = createFetch({
apiKey: process.env.SAPIOM_API_KEY,
agentName: "my-agent",
});
const baseUrl = "https://file-storage.services.sapiom.ai";
// 1. Create an upload slot — returns a presigned PUT URL + file_id
const slotRes = await sapiomFetch(`${baseUrl}/upload`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content_type: "image/png", file_name: "chart.png" }),
});
const { file_id, upload_url, required_headers } = await slotRes.json();
// 2. PUT the bytes straight to GCS (NOT through Sapiom) using the presigned URL
await fetch(upload_url, { method: "PUT", headers: required_headers, body: pngBytes });
// 3. Once verified (poll GET /files until status is "uploaded"), mint a download URL
const dlRes = await sapiomFetch(`${baseUrl}/download/${file_id}`);
const { download_url } = await dlRes.json();
const bytes = await fetch(download_url).then((r) => r.arrayBuffer());

File storage is control-plane only: the gateway issues presigned Google Cloud Storage URLs and tracks file metadata, but the bytes never pass through Sapiom — your client uploads and downloads them directly to/from GCS using the short-lived URLs.

Upload is two steps: create a slot (POST /upload returns a presigned PUT URL valid for 15 minutes), then PUT the bytes to that URL. A background verification sweep then confirms the object landed and flips the file’s status from pending_upload to uploadeduntil it’s verified, GET /download/:id returns 409, so poll GET /files until the file’s status is uploaded before downloading. Download is the reverse — GET /download/:id returns a presigned GET URL.

The PUT (and the download GET) go to the presigned URL with your platform’s native fetch, not sapiomFetch — never send your Sapiom API key to GCS — and pass the slot’s required_headers unmodified.

Files are private by default (only the owning tenant can download) or public (any tenant with a Sapiom API key can download — never anonymous).

Powered by Google Cloud Storage. Presigned URLs are signed locally (V4) and expire after 15 minutes.

Base URL: https://file-storage.services.sapiom.ai

MethodPathDescriptionPricing
POST/uploadCreate a presigned upload slotNo charge*
GET/download/:idMint a presigned download URLNo charge*
GET/filesList your files (metadata only)Free
PATCH/:idChange a file’s visibilityNo charge*
DELETE/:idDelete a fileNo charge*

Endpoint: POST https://file-storage.services.sapiom.ai/upload

Returns a presigned PUT URL (15-minute TTL) and a file_id. PUT the bytes directly to that URL with the returned required_headers.

ParameterTypeRequiredDescription
content_typestringYesMIME type, e.g. image/png
file_namestringNoOriginal file name
visibilitystringNoprivate (default) or public
expected_file_sizenumberNoDeclared size in bytes
{ "content_type": "image/png", "file_name": "chart.png", "visibility": "private" }
{
"file_id": "8f1d1e01-db60-493d-9be8-69d46d95f399",
"upload_url": "https://storage.googleapis.com/...",
"expires_at": "2026-06-25T04:15:00.000Z",
"required_headers": {
"content-type": "image/png",
"x-goog-content-length-range": "0,5368709120"
}
}

Then upload the bytes: PUT {upload_url} with the required_headers and the file as the body.


Endpoint: GET https://file-storage.services.sapiom.ai/download/:id

Returns a presigned GET URL (15-minute TTL) and records one download. Private files are downloadable only by the owning tenant; public files by any tenant.

{ "download_url": "https://storage.googleapis.com/...", "expires_at": "2026-06-25T04:15:00.000Z" }

Endpoint: GET https://file-storage.services.sapiom.ai/files (free)

Returns the calling tenant’s non-deleted files (metadata only — no download URLs). Paginated.

ParameterTypeRequiredDescription
limitnumberNoMax files (1–100, default 50)
offsetnumberNoPagination offset (0–100000, default 0)
GET /files?limit=50&offset=0
{
"files": [
{
"file_id": "8f1d1e01-db60-493d-9be8-69d46d95f399",
"file_name": "chart.png",
"content_type": "image/png",
"visibility": "private",
"status": "uploaded",
"expected_file_size": "20480",
"actual_file_size": "20480",
"created_at": "2026-06-25T04:00:00.000Z",
"uploaded_at": "2026-06-25T04:00:05.000Z",
"deleted_at": null,
"download_request_count": 3
}
],
"limit": 50,
"offset": 0,
"has_more": false
}

A file’s status is one of pending_upload, uploaded, deleted, or error_state. Size fields are returned as strings (the values are 64-bit); download_request_count is a number. file_name, uploaded_at, actual_file_size, and deleted_at are null when not applicable (e.g. before verification).


Endpoint: PATCH https://file-storage.services.sapiom.ai/:id

Flip a file between private and public. Returns the updated metadata.

{ "visibility": "public" }

Endpoint: DELETE https://file-storage.services.sapiom.ai/:id

Deletes the object and soft-deletes the metadata. Idempotent (deleting an already-deleted file succeeds). Only the owning tenant can delete. Returns 204 No Content.


CodeDescription
400Invalid request (e.g. malformed content_type)
401No valid Sapiom tenant identity — authenticate with the Sapiom SDK
403Forbidden — private file owned by another tenant
404File not found, deleted, or in an error state
409File is still pending_upload (bytes not yet verified)
429Rate limit exceeded
LimitValue
Max upload size5 GiB (default)
Presigned URL TTL15 minutes
List page size1–100 (default 50)

File-storage operations are not charged today. Unlike per-call capabilities — which settle a micropayment on each request — file storage is a control-plane service and is not metered per request. When usage-based billing ships it will be metered on usage (e.g. data stored and downloads) rather than charged per call. This page will be updated when it lands.