Files
Upload, organize, and serve files with files.* operations.
Overview
Storage is a home for the files your users upload and the reports, documents, and images your agents produce. Singlebase keeps a record for each object. Uploads go straight from the client to storage, so large files never pass through your API requests.
Key features:
- Direct-to-storage uploads with signed upload tokens
- Batch uploads (up to 10 files by default)
- Logical buckets with allowed file types
- Public or private access, with time-limited signed URLs
- Optional knowledge indexing, so uploaded documents become context for agents and chat
- Profile photo uploads
Use cases: Avatars, attachments, agent-generated reports, knowledge documents for Chat with Documents
On your website: The File Uploader web component runs this upload flow for you.
Access: File storage operations require a signed-in user (bearer token), or a server, root, or permitted agent key. Operations on an existing file check that the user owns it. Admin callers can act on any file.
Operations
| Operation | Description |
|---|---|
files.initiate_upload | Get signed upload credentials |
files.complete_upload | Verify uploaded objects and create file records |
files.fail_upload | Report a failed upload |
files.query | Query file records |
files.get_signed_url | Get a temporary download URL |
files.update | Update metadata or public access |
files.reprocess | Retry ingestion or resume failed indexing |
files.delete | Delete one or more files and their records |
Upload Workflow
Uploading takes three steps:
- Initiate - Call
files.initiate_upload. Keep eachidandupload_token. - Upload -
POSTthe file to the returned storageurlwith the returnedfields. - Complete - Call
files.complete_uploadwith the sameidandupload_token. If the upload failed, callfiles.fail_uploadinstead.
Requirements:
- File storage is enabled for the project.
- The bucket exists, is enabled, and allows the file's type.
- Files are between 100 bytes and 10 MB (10,485,760 bytes).
- Upload credentials and tokens expire after 30 minutes (1800 seconds).
File types:
| Type | Extensions |
|---|---|
IMAGE | png, gif, jpeg, jpg, webp |
DOCUMENT | txt, md, rtf, tex, pdf, pptx, docx, xlsx, odp, odt, ods, epub |
DATA | csv, json, xml, html, htm |
Each bucket lists the types it accepts.
files.initiate_upload
json
{
"operation": "files.initiate_upload",
"payload": {
"files": [
{
"filename": "report.pdf",
"content_type": "application/pdf",
"bucket": "default",
"public_read": false,
"metadata": { "category": "reports" }
}
]
}
}Parameters (each item in files):
| Field | Required | Description |
|---|---|---|
filename | Yes | File name with an allowed extension |
content_type | No | MIME type |
bucket | No | Logical bucket name. Default "default" |
new_name | No | Rename the stored file |
public_read | No | Make the file publicly readable. Default false |
metadata | No | Your own data. Alias info |
options | No | Upload options, such as {"profile_photo": true} |
files is always an array, even for one file. The batch limit defaults to 10.
Response:
json
{
"data": [
{
"id": "7b21f51e730d40d0b0e373ef720e43d5",
"upload": {
"url": "https://storage.example.com/",
"fields": {
"key": "project/7b21f51e730d40d0b0e373ef720e43d5.pdf",
"Content-Type": "application/pdf",
"policy": "<POLICY>",
"x-amz-signature": "<SIGNATURE>"
}
},
"upload_token": "<UPLOAD_TOKEN>"
}
]
}Results keep the input order. Send every fields entry unchanged. Do not decode or modify upload_token. Initiation does not create a file record.
Upload to storage
This browser example uses the singlebase helper. file is a browser File.
javascript
// 1. Initiate
const init = await singlebase('files.initiate_upload', {
files: [{ filename: file.name, content_type: file.type }]
}, idToken);
const item = init.data[0];
// 2. Upload directly to storage
const form = new FormData();
for (const [name, value] of Object.entries(item.upload.fields)) {
form.append(name, value);
}
form.append('file', file); // the file goes last
const uploaded = await fetch(item.upload.url, { method: 'POST', body: form });
// 3. Complete, or report the failure
if (!uploaded.ok) {
await singlebase('files.fail_upload', {
files: [{ id: item.id, upload_token: item.upload_token, error: `HTTP ${uploaded.status}` }]
}, idToken);
throw new Error('Upload failed');
}
const done = await singlebase('files.complete_upload', {
files: [{ id: item.id, upload_token: item.upload_token }]
}, idToken);
console.log(done.data[0].url);Let the browser set the multipart Content-Type. Do not send Singlebase headers to the storage URL.
files.complete_upload
json
{
"operation": "files.complete_upload",
"payload": {
"files": [
{ "id": "7b21f51e730d40d0b0e373ef720e43d5", "upload_token": "<UPLOAD_TOKEN>" }
]
}
}Response:
json
{
"data": [
{
"id": "7b21f51e730d40d0b0e373ef720e43d5",
"owner_id": "user_123",
"filename": "report.pdf",
"filesize": 248311,
"content_type": "application/pdf",
"fileclass": "DOCUMENT",
"file_extension": "pdf",
"bucket": "default",
"status": "completed",
"acl": "private",
"metadata": { "category": "reports" },
"url": "https://storage.example.com/project/7b21f51e730d40d0b0e373ef720e43d5.pdf",
"object_uri": "s3://app-files/project/7b21f51e730d40d0b0e373ef720e43d5.pdf",
"object_key": "project/7b21f51e730d40d0b0e373ef720e43d5.pdf"
}
]
}The response is abbreviated. Completion checks the token, the object's existence, size, and content type, then creates the record. Calling it again with the same token returns the same record.
A private file's url is not directly readable. Use files.get_signed_url.
files.fail_upload
json
{
"operation": "files.fail_upload",
"payload": {
"files": [
{
"id": "7b21f51e730d40d0b0e373ef720e43d5",
"upload_token": "<UPLOAD_TOKEN>",
"error": { "code": "UPLOAD_FAILED", "message": "Connection lost" }
}
]
}
}Each item requires id, upload_token, and error. Returns [{"id": "...", "change": "error"}]. No file record is created.
Batch behavior: Upload batches run concurrently and are not transactional. Some files can complete while others fail.
Profile photos
Set options.profile_photo: true on an image upload. Completing it also sets the owner's profile_photo.
json
{
"operation": "files.initiate_upload",
"payload": {
"files": [
{
"filename": "avatar.jpg",
"content_type": "image/jpeg",
"bucket": "avatars",
"public_read": true,
"options": { "profile_photo": true }
}
]
}
}files.query
Query file records.
json
{
"operation": "files.query",
"payload": {
"owner_id": "user_123",
"content_type": "application/pdf",
"status": "completed",
"page": 1,
"per_page": 20
}
}Parameters (all optional):
| Field | Description |
|---|---|
bucket | Logical bucket |
status | Record status, such as completed |
owner_id, owner_type | Owner |
content_type | MIME type |
storage_backend | Storage backend |
search | Text search. Alias q |
filter | Additional filter |
page, per_page | Defaults 1 and 20 |
limit, offset | Offset pagination |
Returns file records in data and pagination in meta.pagination. To get one record, use filter: {"id": "<FILE_ID>"} and limit: 1.
WARNING
files.query automatically scopes signed-in users to their own files. Another payload-level owner_id or a non-user owner_type returns OWNER_ID_MISMATCH. Server and agent keys can query across owners or choose a payload-level owner_id. Never put owner_id or owner_type inside filter: that returns INVALID_OWNER_ID_FILTER.
Query results are raw file records (for example size_bytes instead of filesize). They do not have exactly the same shape as upload results.
files.get_signed_url
Get a temporary URL for a private file.
json
{
"operation": "files.get_signed_url",
"payload": {
"id": "7b21f51e730d40d0b0e373ef720e43d5",
"signed_ttl": 3600
}
}Response:
json
{
"data": { "url": "https://storage.example.com/...&X-Amz-Signature=..." }
}Send id, or an s3:// object URI in url. signed_ttl defaults to 1800 seconds and is limited to 1 to 604800 seconds (7 days). Ownership is checked.
variant | Object signed |
|---|---|
original (default) | Original uploaded file |
markdown | Stored metadata.md_file from DocLLM conversion |
docv | Stored metadata.docv_file, the full .docv.json result |
For example, use {"id": "file_123", "variant": "markdown", "signed_ttl": 3600} to open the converted Markdown. All variants require the owner or an admin and use the same response and TTL rules. Agent keys need files.read. Conversion URLs are signed from the file's stored metadata; callers cannot supply their path. Missing Markdown returns 404 FILE_MARKDOWN_NOT_FOUND; missing JSON returns 404 FILE_DOCV_NOT_FOUND.
A ready conversion saves src_file, src_bytes, md_file, md_bytes, docv_file and docv_bytes as flat metadata keys returned by files.query, and under __file in private documents and Smart Bucket copies. src_file is the original record's permanent S3 path. Source size comes from DocLLM, or the record's size_bytes when missing. Output paths must be S3 URIs and sizes non-negative integers; missing or invalid output values are null. The Markdown and JSON fields clear when conversion restarts or fails, and are null for script-supplied content. Indexing-only retries preserve them. These fields are gateway-owned and cannot be set through user metadata or content.
Conversion statistics are also stored as flat metadata keys and in __file: src_file_total_pages, md_file_total_pages, total_words and chunk_count. They come from DocLLM's statistics object and must be non-negative integers. Missing page/word values are null; an omitted chunk_count uses the number of saved chunks. Invalid values are null. Statistics clear with conversion outputs on restart, ingestion failure, or script-supplied content, and are gateway-owned.
files.update
Update a file's metadata or access.
json
{
"operation": "files.update",
"payload": {
"id": "7b21f51e730d40d0b0e373ef720e43d5",
"public_read": true,
"metadata": { "category": "approved" }
}
}Parameters:
| Field | Description |
|---|---|
id | File ID (required) |
metadata | Merged into existing metadata (shallow) |
public_read | Change public access on the stored object |
filename (alias name) | Rename the existing file record |
content_type, status, expires_at | Record fields |
_xsync | true refreshes size and status from storage |
_xlock | true locks the file against deletion |
Returns the updated file information. There is no public unlock.
files.reprocess
Retry document processing for an uploaded file you own. Admin and root callers can retry any uploaded file. Agent keys need the files.upload permission and always follow owner retry rules, even with an admin role. Web keys and anonymous callers are refused.
json
{
"operation": "files.reprocess",
"payload": {"id": "file_123"}
}id (alias _id) is required. The file's upload status must be completed. Work runs in the background; the immediate response is:
json
{
"data": {
"id": "file_123",
"doc_processing_status": "pending",
"doc_processing_step": null,
"doc_processing_attempts": 2
}
}If processing failed at indexing and ready, nonblank chunks remain, the response has doc_processing_status: "indexing" and doc_processing_step: "indexing"; only vector indexing runs. Other eligible retries submit a new DocLLM job and ignore the previous job's callback. A retry increments attempts once.
Owners and agents may retry failed or unprocessed files, with four total starts per file (the first run plus three retries). They cannot restart completed processing. Admins and root may restart completed processing and have no attempt limit. Everyone must wait while active processing was updated less than 30 minutes ago; stuck processing at least 30 minutes old may be restarted.
Query the file with files.query to read the seven gateway-managed flags at the top level of its metadata: doc_processing_status, doc_processing_step, doc_processing_error, doc_processing_updated_at, doc_processing_attempts, file_ingested, and file_indexed. Status moves through pending, ingestion, indexing, and completed, or failed with the last step and a short error. Unqueued files start with null status, zero attempts, and false flags. These flags also appear in the private document and Smart Bucket copies under __file. User metadata and script content cannot set them.
Errors:
| Condition | Status | Error |
|---|---|---|
| Unknown file or no write access | 404 | FILE_NOT_FOUND |
| Upload incomplete | 409 | FILE_NOT_COMPLETED |
| Active processing updated less than 30 minutes ago | 409 | FILE_PROCESSING_IN_PROGRESS |
| Completed processing requested by an owner or agent | 403 | FILE_ALREADY_PROCESSED |
| Owner or agent already started processing four times | 429 | FILE_REPROCESS_LIMIT |
Agent lacks files.upload | 403 | AGENT_SCOPE_DENIED |
| Web key or unauthenticated caller | 401/403 | Authentication required |
Caller tries to write processing flags through files.update | 400 | INVALID_FILE_METADATA_FIELD |
files.delete
Delete one file by id, or a batch by ids.
json
{
"operation": "files.delete",
"payload": {
"id": "file_123"
}
}Response:
json
{
"data": {
"ids": ["file_123"],
"change": "deleted"
}
}For batch deletion:
json
{
"operation": "files.delete",
"payload": {
"ids": ["file_123", "file_locked", "file_456"],
"purge": false
}
}Batch response:
json
{
"data": {
"ids": ["file_123", "file_456"],
"failed": [{"id": "file_locked", "error": "FILE_LOCKED"}],
"change": "deleted"
}
}Parameters:
| Field | Description |
|---|---|
id (alias _id) | One file ID; required unless ids is provided |
ids | Between 1 and 100 nonempty file IDs; selects batch behavior |
purge | Permanently remove metadata records; default false |
Behavior:
- Provide
id,ids, or both. Both are merged withidfirst; duplicates are removed in order. The merged selection cannot exceed 100 files. - The stored object is always deleted.
purge(defaultfalse) only controls whether the record is also removed permanently. - A single
idraises on failure. Whenidsis supplied, only successful deletions appear in the response'sids; each failure appears infailed, which is empty when all files succeed. Individual failures do not stop the batch. - Unknown files and files the caller cannot write both report
FILE_NOT_FOUND, so callers cannot probe other users' IDs. Admins can delete other users' files. - Batches run up to 10 files concurrently. Unexpected failures are logged and reported as
FILE_NOT_DELETED. - Deletion also removes the private
__filesdocument and queues vector cleanup. - Stored Markdown and
.docv.jsonoutput objects are deleted too. Cleanup errors are logged without failing the file deletion. - Deleting a profile photo clears the owner's
profile_photo. - Authentication and agent-key permission for
files.deleteremain required.
Errors:
| Condition | Status | Error |
|---|---|---|
Neither id nor ids | 400 | INVALID_PAYLOAD with MISSING_FILE_ID in validation details |
| Empty IDs, empty batch, or more than 100 IDs | 400 | INVALID_PAYLOAD |
| Unknown file or no write access | 404 | FILE_NOT_FOUND |
| Locked file | 403 | FILE_LOCKED |
| Linked documents | 409 | CANT_DELETE_FILE_HAS_LINKED_DOCUMENTS |
| Record deletion fails | 500 | FILE_NOT_DELETED |
For batches, file errors appear as messages in failed rather than rejecting the request. Authentication and payload validation errors still reject the whole request.
DANGER
File bytes cannot be recovered after files.delete, even when purge is false.
Common Errors
| Message | Meaning |
|---|---|
FILE_FORBIDDEN | The user does not own the file |
FILE_BUCKET_NOT_FOUND, FILE_BUCKET_DISABLED | The bucket is missing or disabled |
FILE_TYPE_NOT_ALLOWED | The bucket does not accept this file type |
FILE_UPLOAD_LIMIT_EXCEEDED | Too many files in one upload batch |
UPLOADED_OBJECT_NOT_FOUND | Nothing was uploaded to storage |
CONTENT_TYPE_MISMATCH, INVALID_UPLOADED_OBJECT | The stored object does not match the upload |
FILE_LOCKED, CANT_DELETE_FILE_HAS_LINKED_DOCUMENTS | The file cannot be deleted |
FILE_NOT_UPLOADED, FILE_NOT_CONVERTABLE | The source is incomplete or cannot be converted |
See Responses for the error envelope.
Next Steps
- LLM - Chat with uploaded files as attachments
- Authentication - Get the ID token for uploads
- Agents - Grant file permissions to AI agents