Brault Developers
Guides

Send a transfer

Package Brault files, or files you upload, into one expiring download link.

A transfer packages files into a single public link that expires. It is the API behind Brault's BTransfer: the recipient needs no account, downloads either the whole zip or individual files, and the link stops working on its expiry date. Scopes needed: transfers:write to create, update and expire, transfers:read to list and read back.

1. Send files that are already in Brault

With no uploads declared, one call does everything:

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X POST https://us.api.brault.app/v1/transfers \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8f14e45f-1111-4c1a-9c1e-transfer-create" \
  -d '{
        "files": [{ "id": "<file_id>" }, { "id": "<file_id>", "version_id": "<version_id>" }],
        "folders": ["<folder_id>"],
        "expires_in_days": 14,
        "password": "clientaccess2026"
      }'
{
  "object": "transfer",
  "id": "aB3xY9zQ1p2",
  "url": "https://acme.brault.app/d/aB3xY9zQ1p2",
  "status": "processing",
  "size": 184549376,
  "file_count": 12,
  "folder_count": 2,
  "password_protected": true,
  "expires_at": "2026-09-21T10:00:00.000Z",
  "view_count": 0,
  "download_count": 0,
  "zip_url": null,
  "created_at": "2026-09-07T10:00:00.000Z",
  "updated_at": "2026-09-07T10:00:00.000Z",
  "created_by_id": "<user_id>"
}

url works immediately — recipients can browse and download individual files while the zip is still building. A folder id brings its whole subtree. An id that does not exist or belongs to another brandspace answers 404 not_found for the whole request; a file in this brandspace that the key's creator cannot see is silently left out, and a request where nothing accessible is left answers 400 transfer_no_accessible_content.

version_id is optional and picks an exact processed version of that file instead of the active one. password needs the plan's transfer protection, and any expires_in_days other than the default 7 needs custom expiry — without them the call answers 403 plan_feature_unavailable.

2. Send files you upload yourself

Declare them, and the transfer opens as a draft instead of finishing:

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X POST https://us.api.brault.app/v1/transfers \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8f14e45f-2222-4c1a-9c1e-transfer-draft" \
  -d '{
        "files": [{ "id": "<file_id>" }],
        "uploads": [
          { "name": "brief.pdf", "size": 1048576 },
          { "name": "master.mov", "size": 3221225472 }
        ]
      }'
{
  "object": "transfer",
  "id": "kR7mN2pQ5vL",
  "url": "https://acme.brault.app/d/kR7mN2pQ5vL",
  "status": "awaiting_uploads",
  "size": 0,
  "file_count": 0,
  "folder_count": 0,
  "password_protected": false,
  "expires_at": "2026-09-14T10:00:00.000Z",
  "view_count": 0,
  "download_count": 0,
  "zip_url": null,
  "created_at": "2026-09-07T10:00:00.000Z",
  "updated_at": "2026-09-07T10:00:00.000Z",
  "created_by_id": "<user_id>",
  "uploads": [
    {
      "object": "transfer_upload",
      "id": "<upload_id>",
      "name": "brief.pdf",
      "size": 1048576,
      "method": "put",
      "url": "https://….s3.amazonaws.com/…",
      "part_size": null,
      "status": "pending"
    },
    {
      "object": "transfer_upload",
      "id": "<upload_id>",
      "name": "master.mov",
      "size": 3221225472,
      "method": "multipart",
      "url": null,
      "part_size": 67108864,
      "status": "pending"
    }
  ]
}

The counters read 0 and size is 0 until the draft is finalized — nothing has been packaged yet. Every quota check has already happened, though: the plan's transfer allowance was tested once, up front, against the declared upload bytes plus the Brault content, so a draft that opened will not be refused later for size or quota.

A single PUT (up to 256 MiB)

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X PUT "<url from the upload>" --data-binary @brief.pdf

curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/uploads/<upload_id>/complete \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Send {} as the body: a POST with no body at all carries no Content-Length, and the API answers 400 invalid_request to bodies it cannot size (see Conventions).

{ "object": "transfer_upload", "id": "<upload_id>", "status": "completed" }

The server checks the object is actually there and that its size equals the size you declared. A missing object answers 409 conflict; a different size answers 409 conflict with details.reason: "size_mismatch" — PUT the right bytes and call complete again.

Multipart (above 256 MiB)

Ask for the part URLs, PUT each part, keep every ETag response header, then send them all:

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/uploads/<upload_id>/parts \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "part_numbers": [1, 2, 3] }'
{
  "object": "upload_parts",
  "parts": [
    { "part_number": 1, "upload_url": "https://….s3.amazonaws.com/…" },
    { "part_number": 2, "upload_url": "https://….s3.amazonaws.com/…" },
    { "part_number": 3, "upload_url": "https://….s3.amazonaws.com/…" }
  ]
}
Regionalus.api.brault.appfollows the region selector in the top bar
curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/uploads/<upload_id>/complete \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "parts": [
        { "part_number": 1, "etag": "\"9b2cf5…\"" },
        { "part_number": 2, "etag": "\"1ad0e3…\"" },
        { "part_number": 3, "etag": "\"77c8b1…\"" }
      ] }'

Split the file at exactly part_size bytes (the last part may be smaller) — the server chose that number, and S3 rejects a part list that does not match.

Finalize

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/complete \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8f14e45f-3333-4c1a-9c1e-transfer-finalize" \
  -d '{}'

Every declared upload must be completed first; otherwise the call answers 409 transfer_uploads_pending and details.pending names the ones still outstanding. On success the response is the finished transfer, status: "processing", with its files and folders.

A refusal that arrives before the transfer is written — a pending upload, a missing object, a plan or content refusal — leaves the draft open, so fix the cause and call complete again. A refusal that arrives after it is written, which only the password and expiry step can produce (the plan changed while the draft was open, or the expiry had already been moved), expires the transfer it just created and deletes the draft with it: there is nothing left to retry, so create a new transfer.

3. Wait for the zip

status moves processingready when the zip finishes, and zip_url appears with it. Two ways to find out:

Poll GET /v1/transfers/<id> until status is ready. Every read signs a fresh zip_url and none of them is stored, so read the transfer again when you need a link rather than caching one.

Subscribe to the transfer.ready webhook instead, and read the transfer once when it arrives. See Integrate webhooks; the four transfer events are transfer.created, transfer.ready, transfer.downloaded and transfer.expired.

{
  "object": "transfer",
  "id": "kR7mN2pQ5vL",
  "url": "https://acme.brault.app/d/kR7mN2pQ5vL",
  "status": "ready",
  "size": 4269801472,
  "file_count": 3,
  "folder_count": 0,
  "password_protected": false,
  "expires_at": "2026-09-14T10:00:00.000Z",
  "view_count": 4,
  "download_count": 1,
  "zip_url": "https://cdn.brault.app/…/btransfer_acme_2026-09-07_10-00.zip?Signature=…",
  "created_at": "2026-09-07T10:00:00.000Z",
  "updated_at": "2026-09-07T10:04:12.000Z",
  "created_by_id": "<user_id>",
  "files": [
    { "name": "brief.pdf", "size": 1048576, "mime": "application/pdf", "path": "brief.pdf" },
    { "name": "master.mov", "size": 3221225472, "mime": "video/quicktime", "path": "master.mov" }
  ],
  "folders": []
}

Send url to the recipient — the API never e-mails anyone. zip_url is a direct download for the whole package; url is the page a person opens.

4. Change the expiry or the password, and end it early

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X PATCH https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL \
  -H "Authorization: Bearer $BRAULT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "expires_in_days": 3, "password": null }'

expires_in_days on a PATCH counts from today, and the expiry can be moved once per transfer — a second attempt answers 409 conflict. "password": null removes the password; a string sets one. A transfer that has already expired answers 410 transfer_expired.

Regionalus.api.brault.appfollows the region selector in the top bar
curl -X DELETE https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL \
  -H "Authorization: Bearer $BRAULT_API_KEY"
{ "object": "deleted", "id": "kR7mN2pQ5vL", "deleted": true }

That expires the transfer immediately — recipients lose access at once. On a draft it cancels the upload and releases what was reserved instead.

What to expect afterwards

An expired transfer is deleted for good, and so is one whose zip could not be built: the id stops resolving and GET /v1/transfers/<id> answers 404. Treat a 404 on an id you created as "gone", and subscribe to transfer.expired if you need to know the moment it happens. GET /v1/transfers lists the brandspace's transfers newest first, with any open drafts among them; paging is bounded at 2,000 rows (offset + limit), and a cursor that would read past that answers 400 invalid_request.

On this page