Upload and organize files
Create a library and folder, upload a file, and list what you've stored.
This walks through the smallest end-to-end flow: create a library, create a folder inside
it, upload a file into that folder, then list it back. Scopes needed: files:write to
create and upload, files:read to list.
1. Create a library
A library is the top-level container (called a "workspace" internally — the API only ever calls it a library).
us.api.brault.app— follows the region selector in the top barcurl -X POST https://us.api.brault.app/v1/libraries \
-H "Authorization: Bearer $BRAULT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-1111-4c1a-9c1e-lib-create" \
-d '{"name": "Client Deliverables"}'{
"object": "library",
"id": "<library_id>",
"name": "Client Deliverables",
"description": "",
"emoji": null,
"pinned": false,
"created_at": "2026-09-06T10:00:00.000Z",
"updated_at": "2026-09-06T10:00:00.000Z",
"created_by_id": "<user_id>"
}2. Create a folder
A folder needs exactly one parent: either library_id (a root folder of that library) or
folder_id (a child of that folder).
us.api.brault.app— follows the region selector in the top barcurl -X POST https://us.api.brault.app/v1/folders \
-H "Authorization: Bearer $BRAULT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-2222-4c1a-9c1e-folder-create" \
-d '{"name": "Final Renders", "library_id": "<library_id>"}'3. Upload a file
Uploads are at most four calls, and file bytes never pass through this API — you PUT them straight to S3 with a presigned URL. Start by declaring the name and size:
us.api.brault.app— follows the region selector in the top barcurl -X POST https://us.api.brault.app/v1/uploads \
-H "Authorization: Bearer $BRAULT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-3333-4c1a-9c1e-upload-start" \
-d '{"name": "hero.psd", "size": 18874368, "folder_id": "<folder_id>"}'{
"object": "upload",
"id": "<upload_id>",
"method": "put",
"file_id": "<file_id>",
"version_id": null,
"upload_url": "https://….s3.amazonaws.com/…",
"content_type": "image/vnd.adobe.photoshop",
"part_size": null,
"part_count": null,
"expires_at": "2026-09-07T10:00:00.000Z"
}Files up to 256 MiB get method: "put" — a single PUT with the bytes. upload_url is a
presigned S3 URL signed with content_type baked into the signature, so the PUT must send
that exact value as its Content-Type header — a different (or missing) one fails S3's
signature check:
curl -X PUT "<upload_url from the previous response>" \
-H "Content-Type: <content_type from the previous response>" \
--data-binary @hero.psdFiles larger than 256 MiB get method: "multipart" instead: request presigned URLs per
part with POST /v1/uploads/:uploadId/parts, PUT each part directly to S3, collect the
ETag response header from each PUT, then send them all in the completion call below. See
the Uploads reference for the exact multipart shapes.
Either way, finish with:
us.api.brault.app— follows the region selector in the top barcurl -X POST https://us.api.brault.app/v1/uploads/<upload_id>/complete \
-H "Authorization: Bearer $BRAULT_API_KEY" \
-H "Idempotency-Key: 8f14e45f-4444-4c1a-9c1e-upload-complete"This returns the finished file — the same shape a plain GET /v1/files/:fileId would —
with status: "processing" while thumbnails and previews render.
4. List what's in the folder
us.api.brault.app— follows the region selector in the top barcurl "https://us.api.brault.app/v1/files?folder_id=<folder_id>" \
-H "Authorization: Bearer $BRAULT_API_KEY"GET /v1/files also takes q (keyword/natural-language search), extensions, kind,
created_after/created_before, uploaded_by, min_size/max_size, sort and order
— see the Files reference for the complete parameter list, and
Automate boards for filtering by a property value instead
of a native field.
5. Merge a file into another as new versions
When two files are really two takes of the same asset, merge them: every live version of
the source file becomes a new version of the target file, and the source file is deleted.
Legacy soft-deleted versions of the source, and the comments on them, do not move — they
are deleted along with the source. Scope needed: files:write. You also need permission to
create content where the target lives and to manage content where the source lives.
us.api.brault.app— follows the region selector in the top barcurl -X POST https://us.api.brault.app/v1/files/<target_file_id>/merge \
-H "Authorization: Bearer $BRAULT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-5555-4c1a-9c1e-2b7d1f0a6e42" \
-d '{"source_file_id": "<source_file_id>"}'The response names the merged file and lists the versions it moved, under their new numbers
(abridged). versions reflects the moved versions as they exist at the moment the response
is read; a moved version deleted by another request between the merge and that read is
absent from the list:
{
"object": "file_merge",
"source_file_id": "<source_file_id>",
"file": { "object": "file", "id": "<target_file_id>" },
"versions": [
{ "object": "version", "id": "<version_id>", "file_id": "<target_file_id>", "version_number": 4, "is_active": false },
{ "object": "version", "id": "<version_id>", "file_id": "<target_file_id>", "version_number": 5, "is_active": true }
]
}file is the full file detail object, the same shape GET /v1/files/:fileId returns.
How a merge behaves:
- Numbering. The source's versions keep their order and take the numbers the next uploads to the target would get. The target's own versions keep their numbers.
- Active version. The version that was active in the source becomes the target's active version.
- What moves. Each version keeps its id, its author, its creation date and its comments. Nothing is reprocessed.
- What doesn't. The source is deleted permanently, not trashed. Its board memberships, its file-scoped share link, its shortcuts and its property values are deleted with it. After the merge commits, Brault removes the source's published keys from the CDN on a best-effort basis; a failure is logged, and those keys may stay reachable until they are cleaned up. A merge cannot be undone.
- Same kind only. Both files must be regular files of the same top-level media type (an image into an image, a video into a video), and neither may be in the trash.
- Retries. A second call after a success answers
404, because the source no longer exists. Send anIdempotency-Key, as above, to get the stored response back instead. - Uploads in flight to the target. Complete or abandon any presigned upload URL you issued for the target before merging. A late upload that lands on a version number the merge just reused can overwrite that moved version's bytes.
| Situation | Status | code | details.reason |
|---|---|---|---|
| Source and target are the same file | 400 | invalid_request | same_file |
| A page or a link instead of a regular file | 400 | invalid_request | not_a_regular_file |
| One of the files is in the trash | 400 | invalid_request | file_trashed |
| Different top-level media types | 400 | invalid_request | media_type_mismatch |
| One of the files has no versions | 409 | conflict | no_versions |
| The source, or one of its live versions, is still processing, or one of them has an AI enrichment job still pending | 409 | conflict | source_processing |
| A version upload to the target took one of the computed numbers while the merge ran; nothing changed, retry | 409 | conflict | target_changed |
| The plan does not include file versioning | 403 | plan_feature_unavailable | – |
| Unknown file, another brandspace's file, or no permission on either file | 404 | not_found | – |
A merge sends the webhooks you already handle: one file.version.created per moved version
and one file.deleted for the source. See Webhooks.
The full request and response schema is in the API reference.
Cleaning up
DELETE /v1/files/:fileId and DELETE /v1/folders/:folderId trash by default —
recoverable with the matching POST …/restore call — and hard-delete only when you add
?permanent=true. DELETE /v1/libraries/:libraryId takes no such parameter: it has no
trash step at all and removes every folder and file the library contains immediately. If
you want a safety net before deleting a library, list its contents first.