Brault Developers
Guides

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

Regionalus.api.brault.app— follows the region selector in the top bar
curl -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).

Regionalus.api.brault.app— follows the region selector in the top bar
curl -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:

Regionalus.api.brault.app— follows the region selector in the top bar
curl -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.psd

Files 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:

Regionalus.api.brault.app— follows the region selector in the top bar
curl -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

Regionalus.api.brault.app— follows the region selector in the top bar
curl "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.

Regionalus.api.brault.app— follows the region selector in the top bar
curl -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 an Idempotency-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.
SituationStatuscodedetails.reason
Source and target are the same file400invalid_requestsame_file
A page or a link instead of a regular file400invalid_requestnot_a_regular_file
One of the files is in the trash400invalid_requestfile_trashed
Different top-level media types400invalid_requestmedia_type_mismatch
One of the files has no versions409conflictno_versions
The source, or one of its live versions, is still processing, or one of them has an AI enrichment job still pending409conflictsource_processing
A version upload to the target took one of the computed numbers while the merge ran; nothing changed, retry409conflicttarget_changed
The plan does not include file versioning403plan_feature_unavailable–
Unknown file, another brandspace's file, or no permission on either file404not_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.

On this page