# Authentication and keys (/docs/authentication-and-keys) ## Key format [#key-format] A key looks like `bsk__[]` — `bsk` for "Brault secret key", an 8-character prefix used to look the key up, a 46-character secret, generated with `crypto.randomBytes` and base62-encoded. The trailing `check6` is a 6-character CRC32 checksum, also base62, computed over everything before it — it exists so an automated secret scanner can validate a key's shape offline, without calling the API, the same idea GitHub's own tokens use. It is shown **once**, at creation — there is no roll for `bsk_…` keys; to replace one, create a new key and revoke the old. Afterwards only `name`, a `prefix` and the last 4 characters are ever shown again. Keys are created, renamed, paused, resumed, revoked, transferred and deleted from **Settings → Developers → API keys** in the Brault web app — there is no `/v1` route for key management. Presets in the panel are **Read only** (every `*:read` scope) and **Full access** (every scope); custom selections are allowed too. ## Sending the key [#sending-the-key] ``` Authorization: Bearer bsk_a1b2c3d4_… ``` This is the only transport the API accepts. A key sent any other way — a query parameter, most commonly — is rejected with `401 invalid_auth_transport` and is never echoed back in the error, so it never lands in a log or a browser history from that response. Expiration is optional, set from the panel (30/90/365 days or a specific date). An expired key answers `401 api_key_expired`. ## Who a key acts as [#who-a-key-acts-as] A key **acts as the person who created it**, capped by the key's own scopes: ``` effective permissions = creator's permissions ∩ key's scopes ``` Libraries, folders and boards the creator cannot see are invisible to the key, exactly as they would be to that person in the web app. If the creator leaves the brandspace, is removed, or loses the role that granted an action, the key is **paused automatically** — an owner or admin can **transfer** it to another member, which makes them the new authorizing identity (the original creator stays in the audit trail). ## Scopes [#scopes] Every plan with API access can grant every scope below — plans differ in rate limits, not in which scopes exist. | Scope | Covers | | ------------------------------------ | ----------------------------------------------------------------------------------------------------- | | `files:read` | Libraries, folders, files, versions, downloads, search, brandspace property values (read). | | `files:write` | Create/rename/move/tag/trash/restore/delete files and folders, uploads, versions, property values. | | `boards:read` | Boards, board properties and their options, board files and values (read). | | `boards:write` | Board CRUD, adding/removing files, setting values, managing properties. | | `pages:read` / `pages:write` | Listing/reading pages; creating, publishing/unpublishing, appending blocks. | | `comments:read` / `comments:write` | Comments and replies on files. | | `shares:read` / `shares:write` | Shared links. | | `transfers:read` / `transfers:write` | Transfers: create, list, read, update the expiry or password, expire one early. | | `members:read` | Brandspace members and roles — needed to resolve a `person` value or an `author.user_id` into a name. | | `webhooks:manage` | Webhook endpoints and their deliveries. There is no separate read-only webhooks scope. | A key without the scope a route needs answers `403 insufficient_scope`. ## Errors specific to authentication [#errors-specific-to-authentication] Every failure renders the standard error envelope (see [Conventions](/docs/conventions) § Errors). These codes are specific to resolving the key itself: | Code | HTTP status | Meaning | | ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------- | | `unauthorized` | 401 | No key presented, or the guard could not authenticate it. | | `api_key_invalid` | 401 | The key does not resolve to an active row. | | `api_key_paused` | 401 | The key exists but is paused — manually, or automatically because its creator lost access. | | `api_key_revoked` | 401 | The key has been revoked. Terminal. | | `api_key_expired` | 401 | Past its `expires_at`. | | `api_key_orphaned` | 401 | The creator no longer has brandspace access; an admin must transfer the key. | | `invalid_auth_transport` | 401 | The key was sent somewhere other than the `Authorization: Bearer` header. | | `insufficient_scope` | 403 | The key's scopes do not cover this endpoint. | | `forbidden` | 403 | The creator must satisfy a brandspace security requirement first — in practice, mandatory two-factor enrollment. | | `plan_without_api` | 403 | The brandspace's plan does not include API access (Free). `details.upgrade_url` points at the pricing page. | A denial that is really about **which id** you can reach — rather than a fact about the key itself — is never a `403`. See [Conventions](/docs/conventions) § Errors for why a cross-tenant id and a same-tenant permission denial both answer a plain `404`. # Changelog (/docs/changelog) Per [Conventions](/docs/conventions) § Versioning, additive changes — new fields, new endpoints, new optional parameters, new enum values — never appear here as a breaking change and never bump the path version. A genuinely breaking change will get its own dated entry and ship as `/v2` alongside `/v1`. ## 2026-09-09 — Central and regional hosts [#2026-09-09--central-and-regional-hosts] Additive, no version bump. **Nothing you have built stops working**, and no key needs re-minting. `/v1` now names two hosts instead of one. `api.brault.app` is the **central** host and serves 7 paths — `/v1/me`, `/v1/usage`, `/v1/events`, `/v1/brandspace`, `/v1/members`, `/v1/members/{user_id}` and `/v1/roles`. Everything else is **regional** and belongs on `https://.api.brault.app`, which is `https://us.api.brault.app` today. The reason is data residency: content is answered by a machine in the brandspace's own region. See [Central and regional hosts](/docs/conventions#central-and-regional-hosts). * **Your key now prints its host.** New keys read `bsk_us_…`; the segment after `bsk_` is the regional host to dial. A key minted before this has no such segment, means `us`, and keeps working forever — there is no sunset and no re-mint. * **`GET /v1/me` answers the question directly.** It gained `hosts.central`, `hosts.regional`, `brandspace.region`, `key.region` and `key.region_stale`. The brandspace's region is authoritative; the one printed in the key is a hint. * **Calling the central host for a regional path still works today.** It will start carrying `Deprecation: true`, a `Sunset` date and a `Link` header pointing at the Conventions page. *The `Sunset` date is not yet set; it will be announced here, in its own entry, before any header carries it.* * **A new error code, `421 misdirected_request`.** It cannot occur today — there is one region — and appears only when a host's region is not the brandspace's. `details.host` names the host that will answer. * **Every example on this site is now labeled** Central or Regional and shows the host it calls, and the [API reference](/docs/reference) marks each operation's plane. Also now documented, both already live: `POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver` and `POST /v1/webhooks/{webhook_id}/replays` — see the [Webhooks reference](/docs/reference/webhooks). ## 2026-09-07 — Transfers [#2026-09-07--transfers] Additive, no version bump (see [Conventions](/docs/conventions) § Versioning). * **Transfers** — `POST /v1/transfers` packages Brault files and folders, files you upload, or both, into one expiring public link. Uploads follow the same single-PUT and multipart flow as `/v1/uploads`, and `POST /v1/transfers/:id/complete` finalizes the package. List, read, update the expiry or password, and expire one early. See [Send a transfer](/docs/guides/send-transfers) and the [Transfers reference](/docs/reference/transfers). * **Two new scopes** — `transfers:read` and `transfers:write`. Existing keys do not get them automatically: edit the key in `Settings → Developers` and tick the Transfers row. * **Four new webhook events** — `transfer.created`, `transfer.ready`, `transfer.downloaded` and `transfer.expired`, bringing the catalogue to 58. A key subscribed with `["*"]` already receives them. ## v1.0.0 — 2026-09-06 [#v100--2026-09-06] Initial public release of `/v1`. * **Content** — libraries, folders, files, versions, uploads (single-PUT and multipart), downloads, keyword and natural-language search, importing a file from a public URL. Read-only brandspace, member and role lookups. * **Boards** — boards, board properties and options, brandspace-wide properties and options, setting and reading property values on files, filtering `GET /v1/files` and querying a board with the JSON filter grammar. * **Review, pages and shares** — comments and replies (with a CSV export), pages (create, read as JSON or Markdown, append blocks, publish/unpublish), and shared links (create, read, update, delete). * **Webhooks** — endpoints, deliveries, the event catalogue (`GET /v1/events`), signed payloads, and the `Settings → Developers → Webhooks` panel. * **Platform** — API keys with per-area scopes, per-brandspace rate limits and monthly quotas, cursor pagination, a uniform error envelope, and `Idempotency-Key` support on every `POST`. See [Getting Started](/docs) for the base URL and your first request, or jump straight to the [API Reference](/docs/reference) for every operation. # Conventions (/docs/conventions) These rules hold across every `/v1` route. Endpoint-specific behavior lives on each resource's own reference page and guide. ## Central and regional hosts [#central-and-regional-hosts] `/v1` is served on two kinds of host, and **every path belongs to exactly one of them**: | Plane | Host | What it serves | | ------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Central** | `https://api.brault.app` | The key's own context and the brandspace registry: identity, plan, usage, members, roles, the event catalogue. One origin worldwide. | | **Regional** | `https://us.api.brault.app` (`https://.api.brault.app`) | Everything carrying your content — files, folders, libraries, boards, pages, properties, uploads, downloads, transfers, shared links, webhooks, search. | The regional host exists for **data residency**: your brandspace's bytes are answered by a machine in your brandspace's own region, so publishing a content path on the central host would move data across a border the day a second region exists. The split is per **operation**, never per path prefix — `/v1/files/*` is regional and `/v1/me` is central, and both live under `/v1`. ### The 7 central paths [#the-7-central-paths] There are no others. Everything else under `/v1` is regional. | Path | Method | Why central | | ----------------------- | ------ | ----------------------------------------------------------------- | | `/v1/me` | `GET` | The key's own context, and where you look up your hosts. | | `/v1/usage` | `GET` | Counters and plan limits; no content. | | `/v1/events` | `GET` | The webhook event-type catalogue — a fixed list. | | `/v1/brandspace` | `GET` | The brandspace registry row (`id`, `name`, `username`, `region`). | | `/v1/members` | `GET` | Brandspace membership. | | `/v1/members/{user_id}` | `GET` | Brandspace membership. | | `/v1/roles` | `GET` | Brandspace roles and their permissions. | `GET /v1/health` is the **single exception**: it answers on both hosts. It reads no table, resolves no key and returns a literal, so it carries no data that could cross a border. It sets no precedent — any path that touches a row belongs to exactly one plane. ### Which host is mine? [#which-host-is-mine] Three ways, in order of directness: 1. **Look at the key.** `bsk_us_…` means `us.api.brault.app`. A key minted before regions existed has no region segment and means `us`. 2. **Ask the API.** `GET /v1/me` returns `hosts.central` and `hosts.regional` for your key, plus the authoritative `brandspace.region`. Nothing else has to be derived. 3. **Read the label on the example.** Every code block on this site is marked **Central** or **Regional** and shows the hostname above it. The brandspace's region is **authoritative**; the region printed in the key is **advisory** — it tells you which host to dial, it never decides which host may answer. A brandspace that moves region re-mints no keys: every existing key keeps authenticating, on the new host, from the first request. `GET /v1/me` reports the stale hint as `key.region_stale`. ### Calling the wrong host [#calling-the-wrong-host] Two different failures, treated differently, because only one of them is a residency problem: * **Plane mismatch** — a regional path called on the central host, or the other way round, while your brandspace is in the region that host serves. It is served today, and while it is being phased out it answers with `Deprecation: true`, a `Sunset` date and a `Link` header pointing back at this page. The `Sunset` date is announced in the [changelog](/docs/changelog) before it takes effect. * **Residency mismatch** — the host's region is not your brandspace's region. Refused with `421 misdirected_request` from the day a second region exists, with no grace period: serving it would move your data across a border. `details.host` names the host that will answer and `details.region` your brandspace's region, so a client can correct itself without a lookup. An unrecognized host is never refused — the check fails open, so a health check that reaches an origin directly is never turned into a `421`. ## Wire format [#wire-format] * JSON, `snake_case` everywhere — request bodies and responses alike. * Every object carries `"object": "file" | "folder" | "board" | …` naming its type. * Every list looks the same: ```json { "object": "list", "data": [ … ], "has_more": false, "next_cursor": null } ``` * Dates are ISO 8601 UTC (`2026-09-03T10:15:00.000Z`). Sizes are bytes as integers; durations are milliseconds. Ids are opaque strings — never parse them for meaning. ## Pagination [#pagination] Every list takes `limit` (1–100, default 50) and an opaque `cursor`: ```bash curl "https://us.api.brault.app/v1/files?limit=25" \ -H "Authorization: Bearer $BRAULT_API_KEY" # -> follow with ?cursor= while has_more is true ``` The cursor is opaque **by contract, not by cryptography** — it is base64url-encoded JSON that can technically be decoded, but its shape may change without a version bump and nothing is gained by forging it (the query it indexes is already scoped to your brandspace). If a cursor was issued for a different set of filters than the request now carries — you changed `limit`, added a filter, and so on — the API answers `400 invalid_request` rather than silently reinterpreting it: "The cursor was issued for a different query." The same error covers a cursor that fails to decode at all. **Always page until `has_more` is `false`.** A few routes (documented on their own pages) can return fewer than `limit` items on a page while more remain — never treat a short page as the end. ## Errors [#errors] Every failure renders the same envelope: ```json { "object": "error", "status": 404, "code": "not_found", "message": "…", "request_id": "req_…", "details": {} } ``` `details` is omitted when there's nothing structured to add. Codes are stable, lowercase, and safe to branch on — `message` is a human sentence that can change. | Code | HTTP status | Meaning | | ------------------------------------------------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_request` | 400 | Generic malformed request. | | `validation_error` | 400 | Body or query failed validation. `details.errors[{ field, reason }]`. | | `unauthorized` | 401 | No key presented, or it could not be authenticated. | | `payment_required` | 402 | The brandspace's subscription is unpaid. | | `insufficient_scope` / `forbidden` / `plan_without_api` / `plan_feature_unavailable` | 403 | A fact about the **key** or its plan — never about a specific id. | | `not_found` | 404 | The id does not exist, belongs to another brandspace, or your key's creator lacks permission for it — see below. | | `conflict` | 409 | The request conflicts with the resource's current state. | | `idempotency_conflict` | 409 | The same `Idempotency-Key` was replayed with a different body. | | `rate_limited` / `quota_exceeded` | 429 | A bucket or the monthly quota was hit. `Retry-After` header. | | `misdirected_request` | 421 | The host you called does not serve this brandspace's region. `details.host` names the one that does — see [Central and regional hosts](#central-and-regional-hosts). | | `payload_too_large` | 413 | Body over the 1 MB cap. | | `unsupported_media_type` | 415 | Anything other than `application/json`. | | `internal_error` | 500 | Unhandled exception. | | `service_unavailable` | 503 | The backend cannot serve the request right now. | See each resource's reference page for the full list of `api_key_*` and resource-specific codes; [Authentication and keys](/docs/authentication-and-keys) covers the auth-specific ones. ### The 404 rule [#the-404-rule] **Every entity-scoped route answers `404 not_found` for an id it will not serve, whatever the reason**: the id does not exist, it belongs to another brandspace, or your key's creator simply doesn't have permission for it. All three are byte-identical on the wire. This is deliberate — the alternative (a `403` that confirms an id exists but you can't touch it) would let a key discover ids outside its own brandspace. If you're debugging a permission problem and keep seeing `404`, check **Settings → Developers** — that's the one place the real reason is visible. ## Request id [#request-id] Every response carries `X-Request-Id` — send your own (≤ 64 characters) or one is generated for you. It's stored with the request log entry, so a support conversation can start from a single header value. ## Idempotency [#idempotency] Send `Idempotency-Key` (≤ 128 characters) on any `POST` that could duplicate a real-world effect if retried blindly — creating a library, folder, board, comment, reply, share link or webhook; adding a file to a board; starting an upload or an import: ```bash curl -X POST https://us.api.brault.app/v1/libraries \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Idempotency-Key: 8f14e45f-ceea-4c1a-9c1e-example" \ -H "Content-Type: application/json" \ -d '{"name": "Client Deliverables"}' ``` * A replay of the same key **and the same body** returns the original response again, with `Idempotency-Replayed: true`. * The same key with a **different** body answers `409 idempotency_conflict`. * A request still in flight under that key answers `409 conflict` — retry shortly. * A key over 128 characters is refused outright, before any handler work. Every `POST` honors the header, not just the ones listed above — sending it on a naturally-idempotent action (a move, a copy, publish/unpublish) is harmless. ## Body limits [#body-limits] `/v1` accepts only `Content-Type: application/json` on `POST`/`PUT`/`PATCH` — `multipart/form-data` included — anything else is `415 unsupported_media_type`. File bytes always go straight to S3 through a presigned URL; see [Upload and organize files](/docs/guides/upload-and-organize). A body over 1 MB is `413 payload_too_large`; a request with no declared `Content-Length` (a chunked body) is `400 invalid_request`, checked before the media type. ## Versioning [#versioning] The major version lives in the path (`/v1`). Additive changes — new fields, new endpoints, new optional parameters, new enum values documented as extensible — never bump it. A breaking change ships as `/v2`, served alongside `/v1` for at least 12 months, with deprecations announced in the [changelog](/docs/changelog) and signaled with `Deprecation` and `Sunset` headers on the affected routes. # Get an API key (/docs/get-an-api-key) API keys are created in the Brault web app, never through the API. You need a plan with API access (**Lite** or higher; the Free plan shows the Developers section locked, with the "Available from Lite" line and an upgrade button) and the **Developers → manage** permission, which owners and admins have by default and custom roles can be granted. ## 1. Open Developers in Settings [#1-open-developers-in-settings] Open Settings → Developers → API keys The link goes to `brault.app/settings/developers?tab=keys`. Signed out, you land on the login page and come back afterwards; signed in, you are taken to your brandspace with the **Developers** section open on the **API keys** tab. You can also get there by hand: open **Settings** from your brandspace menu, then pick **Developers** in the sidebar. The Developers section of Settings, on the API keys tab, before any key exists ## 2. Create the key [#2-create-the-key] Click **Create API key**. The **Create API key** dialog asks for three things: * **Name (optional)** — something that tells you where the key is used, such as "Zapier automations". It is what the keys table shows. * **Permissions** — a preset, **Read only** (every `*:read` scope) or **Full access** (every scope), or **Custom**, which opens read and write checkboxes per area (files, boards, comments, pages, shares, transfers, members, webhooks…). A key can never do more than the person who created it can do in the app — scopes only narrow that. * **Expires** — **Never**, **30 days**, **90 days**, **1 year**, or **On a date**. Press **Create key**. The Create API key dialog with a name filled in and the Permissions menu open The **Create API key** button is disabled once the brandspace holds as many active keys as its plan allows; revoke one you no longer use to free a slot. ## 3. Save the key — it is shown once [#3-save-the-key--it-is-shown-once] The **Save your API key** dialog shows the full key exactly once. Press **Copy**, store it in your secret manager or as an environment variable, tick **I have stored this key**, and continue. Afterwards the panel only ever shows the `bsk_` prefix and the last four characters. The Save your API key dialog with the Copy button and the confirmation checkbox ```bash title="Shell" export BRAULT_API_KEY="bsk_<8-character prefix>_<46-character secret>" ``` There is no "roll" for a key. If you lose one or suspect it leaked, revoke it in the table and create a new one. ## 4. Check it works [#4-check-it-works] ```bash title="cURL" curl https://api.brault.app/v1/me \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` The response names your brandspace, the key, its scopes and the limits of your plan — the same call [Getting started](/docs) uses as the first request. A `401` means the key was mistyped, paused, revoked or expired; see [Authentication and keys](/docs/authentication-and-keys#errors-specific-to-authentication) for the exact codes. ## Managing keys later [#managing-keys-later] The **API keys** table lists every key with its name, the `bsk_` prefix and last four characters, who created it, when, when it was last used, when it expires, its permissions and its status (active, paused, revoked or expired). From a key's menu you can **Edit** the name and scopes, **Pause** and **Resume** it, **Transfer** it to another member, **Revoke** it, and remove a revoked key from the list. The API keys table with one active key A key acts as its creator. If that person loses access to the brandspace, the key pauses automatically until an owner or admin transfers it to someone else. # Getting Started (/docs) The Brault public API lets you automate libraries, files, boards, pages, reviews, sharing and transfers in your brandspace. It is a JSON HTTP API served on **two hosts**: | Host | What it serves | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `https://api.brault.app/v1` | **Central.** Your key's own context, plan, usage, members and roles — 7 paths, the same host worldwide. | | `https://us.api.brault.app/v1` | **Regional.** Everything that holds your content: files, folders, libraries, boards, pages, transfers, webhooks, search. Answered by the region your brandspace lives in, and by no other. | Regional paths are served next to your data, so an EU brandspace's files are answered by an EU machine and never by a US one. You do not have to work it out: every code example on this site is labeled **Central** or **Regional** and shows the host to call, and [`GET /v1/me`](/docs/reference/meta/MetaController_me) returns both hosts for your key in its `hosts` field. [Conventions](/docs/conventions#central-and-regional-hosts) has the full table of the 7 central paths. Every route lives under `/v1` — there is no `/api` segment in the public URL. The version is part of the path: additive changes (new fields, endpoints, optional parameters) never bump it, and a breaking change would ship as `/v2` served alongside `/v1` for at least 12 months. See [Conventions](/docs/conventions) for the full versioning policy. ## 1. Get an API key [#1-get-an-api-key] API access starts at the **Lite** plan — the Free plan has no API access. Keys are created and managed from **Settings → Developers → API keys** in the Brault web app, never through the API itself. A key looks like: ``` bsk__<8-character prefix>_<46-character secret> ``` The `` segment names the regional host to call — `bsk_us_…` means `us.api.brault.app`. A key created before regions existed has no such segment (`bsk__`); it keeps working forever and means `us`. It is shown in full exactly once, at creation. Store it like any other secret. [Get an API key](/docs/get-an-api-key) walks through the panel step by step; see [Authentication and keys](/docs/authentication-and-keys) for scopes, lifecycle and error codes. ## 2. Make your first request [#2-make-your-first-request] Every request authenticates with `Authorization: Bearer ` — never as a query parameter. Here is the simplest possible call, which just confirms who the key is and what it can do: ```bash title="cURL" curl https://api.brault.app/v1/me \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` ```js title="JavaScript" const res = await fetch('https://api.brault.app/v1/me', { headers: { Authorization: `Bearer ${process.env.BRAULT_API_KEY}` }, }); const me = await res.json(); console.log(me.brandspace.name, me.plan.type); ``` ```python title="Python" import os import requests res = requests.get( "https://api.brault.app/v1/me", headers={"Authorization": f"Bearer {os.environ['BRAULT_API_KEY']}"}, ) me = res.json() print(me["brandspace"]["name"], me["plan"]["type"]) ``` A successful response looks like this (field names and shapes below are taken directly from the published OpenAPI document, `GET /v1/me`): ```json { "object": "api_key_context", "brandspace": { "id": "clx1…", "name": "Acme Studio", "username": "acme", "region": "us" }, "key": { "id": "…", "name": "CI integration", "display": "bsk_us_a1b2c3d4_…wxyz", "scopes": ["files:read", "files:write"], "region": "us", "region_stale": false, "expires_at": null }, "created_by": { "id": "…", "name": "Jamie Rivera" }, "plan": { "type": "pro", "name": "Pro" }, "limits": { "burst_per_second": 10, "sustained_per_minute": 300, "monthly_requests": 100000, "uploads_per_day": 2000, "downloads_per_day": 10000, "search_per_minute": 30 }, "hosts": { "central": "https://api.brault.app", "regional": "https://us.api.brault.app" } } ``` `hosts` is the answer to "which host do I call?", read straight from your own key: `hosts.central` for the 7 central paths, `hosts.regional` for everything else. `brandspace.region` is authoritative and decides which host may answer; `key.region` is the hint printed inside the key string, and `key.region_stale` is `true` only when the brandspace moved region after the key was minted — the key still works, the printed hint is what went out of date. Every object on the wire carries an `"object"` field identifying its type, and every response is `snake_case` — see [Conventions](/docs/conventions) for the wire format, pagination and error shape shared by every route. ## 3. Where to go next [#3-where-to-go-next] * [Authentication and keys](/docs/authentication-and-keys) — key format, scopes, lifecycle. * [Plans and limits](/docs/plans-and-limits) — rate limits, quotas, and the plan table. * [Conventions](/docs/conventions) — pagination, errors, idempotency, versioning. * [Guides](/docs/guides/upload-and-organize) — end-to-end walkthroughs for common tasks. * [Webhooks](/docs/webhooks) — event catalogue and signature verification. * [API Reference](/docs/reference) — every `/v1` operation, generated from the OpenAPI document. # Plans and limits (/docs/plans-and-limits) Request volume is bounded **per brandspace**: every API key that brandspace has issued shares the same limits (there is no separate per-key sub-limit). ## Buckets [#buckets] | Bucket | Window | Counts against it | | ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `burst` | per second | Every request. | | `sustained` | per minute | Every request. | | `search` | per minute | `GET /v1/search`, `GET /v1/files` when called with `q`, `GET /v1/files/:id/similar`, `POST /v1/boards/:id/query` when called with `q`. | | `uploads` | per day | Upload initiations (`POST /v1/uploads`), file imports (`POST /v1/files/import`). | | `downloads` | per day | `POST /v1/downloads`, `GET /v1/files/:id/download`. | | monthly quota | calendar month | Every request. | ## Response headers [#response-headers] Every `/v1` response carries: * `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` — the **sustained** (per-minute) window, in epoch seconds. * `X-Quota-Limit`, `X-Quota-Remaining`, `X-Quota-Reset` — the **monthly** quota. A rejection still carries all six headers, whichever specific bucket was actually denied, plus `Retry-After` and one of: * `429 rate_limited` — `details.bucket` names the bucket that was hit. * `429 quota_exceeded` — the monthly request quota is used up. ## Plan table [#plan-table] API access starts at **Lite**; **Free has no API access** at all and answers `403 plan_without_api` with `details.upgrade_url`. These limits are data, not code — they change without a deploy, so treat the table below as a guide and `GET /v1/me` (see [Getting Started](/docs)) as the source of truth for your own brandspace. | | Lite | Pro | Growth | Custom | | ---------------------------- | ----- | ------- | --------- | -------- | | Burst (req/s) | 2 | 10 | 25 | contract | | Sustained (req/min) | 30 | 300 | 1,000 | contract | | Monthly requests | 1,000 | 100,000 | 1,000,000 | contract | | Upload initiations / day | 20 | 2,000 | 20,000 | contract | | Download URL issuances / day | 100 | 10,000 | 100,000 | contract | | Search (NL) / min | 2 | 30 | 120 | contract | | Active keys | 1 | 5 | 20 | contract | | Webhook endpoints | 1 | 3 | 10 | contract | | Request-log retention (days) | 7 | 30 | 90 | contract | ## When the limiter is unavailable [#when-the-limiter-is-unavailable] If the counter store is unreachable, requests are allowed through rather than blocked — a counter outage should never take the whole API down. The one exception is authentication itself: a database outage while a key is being resolved always answers `500 internal_error`, it never fails open. ## Failed-authentication throttling [#failed-authentication-throttling] Independent of the buckets above: every request whose key fails to authenticate (any `401 api_key_*` outcome) counts against a per-IP counter in a rolling 5-minute window. At 20 failures, that IP gets `429 rate_limited` (`details.bucket: "auth"`) without the key even being looked up, until the window resets. Only failures count — a valid key from the same IP is never throttled by this counter. # Webhooks (/docs/webhooks) See [Integrate webhooks](/docs/guides/integrate-webhooks) for how to register an endpoint and what delivery behavior (retries, auto-disable) to expect. This page covers what you receive and how to trust it. ## Headers on every delivery [#headers-on-every-delivery] | Header | Contents | | ------------------------- | -------------------------------------------------------------- | | `Brault-Signature` | `t=,v1=` — see verification below. | | `Brault-Webhook-Id` | The endpoint that received this delivery. | | `Brault-Event-Id` | Matches `id` in the payload — the deduplication key. | | `Brault-Event-Type` | One catalogue token, e.g. `file.created`. | | `Brault-Delivery-Id` | This specific delivery attempt's record id. | | `Brault-Delivery-Attempt` | `1`–`6`. | | `X-Request-Id` | Correlates this delivery with the platform request log. | | `User-Agent` | `Brault-Webhooks/1.0` | | `Content-Type` | `application/json; charset=utf-8` | | `Content-Length` | Byte length of the exact signed body — never chunked transfer. | ## Verifying the signature [#verifying-the-signature] The signed material is `.` — the **exact bytes** you received, before any re-serialization. Read the raw body first; don't verify against `JSON.stringify` of a parsed object, since key order or whitespace differences would silently break every signature. ``` Brault-Signature: t=1725609600,v1=5257a869e7bfbe86... ``` 1. Split the header on `,`, then split each part on the first `=`. 2. Reject the delivery if `|now − t| > 300` seconds (5 minutes) — this bounds replay of an intercepted request. 3. Compute `hex(hmac-sha256(secret, "."))`. 4. Accept if it matches **any** `v1` entry present. There is normally exactly one — but for 24 hours after you roll a webhook's secret, deliveries carry **two** `v1` entries (current secret first, then the previous one), so that you can pick up the new secret at your own pace without dropping events mid-rotation. This bash version is a debugging aid for a quick terminal check, not a template for production code: it doesn't compare in constant time, and the secret is briefly visible on the command line (`ps`) while it runs. For production verification, use the JavaScript or Python implementation below. ```bash #!/usr/bin/env bash # Usage: verify.sh "" body_file="$1" signature_header="$2" secret="$3" t=$(echo "$signature_header" | grep -oE 't=[0-9]+' | head -1 | cut -d= -f2) now=$(date +%s) age=$(( now - t )) [ "${age#-}" -gt 300 ] && { echo "signature too old" >&2; exit 1; } expected=$(openssl dgst -sha256 -mac HMAC -macopt "key:$secret" -binary \ < <(printf '%s.' "$t"; cat "$body_file") \ | od -An -tx1 | tr -d ' \n') echo "$signature_header" | grep -oE 'v1=[0-9a-f]+' | cut -d= -f2 | grep -qx "$expected" \ && echo "valid" \ || { echo "invalid" >&2; exit 1; } ``` ```js import crypto from 'node:crypto'; const TOLERANCE_SECONDS = 5 * 60; // rawBody must be the exact bytes/string of the request body — capture it // before any JSON.parse (e.g. express.raw() ahead of your JSON middleware). export function verifyBraultSignature(rawBody, signatureHeader, secret) { const entries = signatureHeader.split(',').map((part) => part.split('=')); const t = entries.find(([key]) => key === 't')?.[1]; const signatures = entries.filter(([key]) => key === 'v1').map(([, v]) => v); if (!t || signatures.length === 0) return false; const age = Math.abs(Math.floor(Date.now() / 1000) - Number(t)); if (age > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac('sha256', secret) .update(`${t}.${rawBody}`) .digest('hex'); return signatures.some( (sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)), ); } ``` ```python import hashlib import hmac import time TOLERANCE_SECONDS = 5 * 60 def verify_brault_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: """raw_body must be the exact bytes of the request body, read before any JSON parsing.""" parts = [p.split("=", 1) for p in signature_header.split(",")] timestamp = next((v for k, v in parts if k == "t"), None) signatures = [v for k, v in parts if k == "v1"] if timestamp is None or not signatures: return False if abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS: return False signed_payload = f"{timestamp}.".encode() + raw_body expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, sig) for sig in signatures) ``` ## The event envelope [#the-event-envelope] Every delivery's body looks like this: ```json { "id": "evt_c0ffee…", "object": "event", "type": "file.created", "occurred_at": "2026-09-05T10:15:00.000Z", "brandspace_id": "clx…", "actor": { "type": "user", "id": "clu…" }, "data": { "object": "file", "id": "clf…", "name": "hero.psd", "library_id": "clw…", "folder_id": null }, "attempt": 1 } ``` `data` is a minimal snapshot (ids and names, not the full resource) — re-fetch the resource from the API if you need more. `actor.type` is one of four values: `user` (the common case, including events caused by your own key's requests — the key acts as its creator), `api_key` (only on `webhook.test` and the two import events), `system` (an automated cause with no person behind it — crons, cascades, the media-processor callback — always `id: null`), or `anonymous` (an unauthenticated visitor: a shared-link visitor or a transfer recipient, always `id: null`) — see [Integrate webhooks](/docs/guides/integrate-webhooks) for what this means for de-duplicating your own writes. ## Event catalogue [#event-catalogue] `events: ["*"]` subscribes to every event below, including any a later version adds — the catalogue only ever grows additively. Fetch it live (any scope will do — the route still needs a key) at `GET /v1/events`, or see the [Meta reference](/docs/reference/meta) for the exact shape of each row. ### Files (10) [#files-10] `file.created`, `file.updated`, `file.processed`, `file.moved`, `file.trashed`, `file.restored`, `file.deleted`, `file.version.created`, `file.version.activated`, `file.version.deleted` ### Folders and libraries (9) [#folders-and-libraries-9] `folder.created`, `folder.updated`, `folder.moved`, `folder.trashed`, `folder.restored`, `folder.deleted`, `library.created`, `library.updated`, `library.deleted` ### Boards and properties (13) [#boards-and-properties-13] `board.created`, `board.updated`, `board.deleted`, `board.file.added`, `board.file.removed`, `board.property.created`, `board.property.updated`, `board.property.deleted`, `board.file.property_updated`, `property.created`, `property.updated`, `property.deleted`, `property.value_updated` ### Comments (8) [#comments-8] `comment.created`, `comment.updated`, `comment.resolved`, `comment.reopened`, `comment.deleted`, `reply.created`, `reply.updated`, `reply.deleted` ### Pages (5) [#pages-5] `page.created`, `page.updated`, `page.published`, `page.unpublished`, `page.deleted` `page.updated` means the page's **content** was saved — a page rename, tag change, move or restore arrives as the corresponding `file.*` event instead, with `data.file_type: "canvas"` telling you it was a page. ### Sharing and members (6) [#sharing-and-members-6] `shared_link.created`, `shared_link.updated`, `shared_link.deleted`, `member.added`, `member.removed`, `member.role_changed` ### Imports (2) [#imports-2] `import.completed`, `import.failed` — report the terminal state of a `POST /v1/files/import` job. A successful import also produces a normal `file.created`; the two are not the same signal and both fire. ### Transfers (4) [#transfers-4] `transfer.created`, `transfer.ready`, `transfer.downloaded`, `transfer.expired` — the life of a transfer link. `transfer.ready` is the one that says the zip finished and `zip_url` now resolves; `transfer.expired` fires whenever the transfer stops being available, whether it expired on its own, was ended early, or its zip could not be built. A transfer you ended early with `DELETE` gets `transfer.expired` twice: once at the call and once more when the nightly cleanup removes the row, with a different event id each time, so dedupe on `data.id`. `transfer.downloaded` carries `actor.type: "anonymous"` for a recipient with no account, which is the usual case. ### Platform (1) [#platform-1] `webhook.test` — the synthetic event `POST /v1/webhooks/:id/test` sends. # Automate boards (/docs/guides/automate-boards) Boards group files with custom properties — statuses, people, dates, tags — independent of where the files live in your libraries. Scopes needed: `boards:write` to create the board, add files and set values, and `boards:read` to query it back. Adding a file to a board additionally requires the key creator's own edit role on that file's library or folder — see the note on file access below. ## 1. Create a board with a status property [#1-create-a-board-with-a-status-property] `default_property_type: "status"` seeds one `status` property with the three default options `To Do`, `In Progress`, `Done` — so you don't need a second call just to get started. ```bash curl -X POST https://us.api.brault.app/v1/boards \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-5555-4c1a-9c1e-board-create" \ -d '{"name": "Client Reviews", "default_property_type": "status"}' ``` ```json { "object": "board", "id": "", "name": "Client Reviews", "color": "#E2E8EE", "emoji": null, "logo_url": null, "pinned": false, "my_access": "manage", "created_at": "2026-09-06T10:00:00.000Z", "updated_at": "2026-09-06T10:00:00.000Z", "created_by_id": "", "properties": [ { "object": "property", "id": "", "scope": "board", "board_id": "", "name": "Status", "type": "status", "selection_mode": null, "options": [ { "object": "option", "id": "", "label": "To Do", "color": "#E2E8EE", "hidden": false, "position": 0 }, { "object": "option", "id": "", "label": "In Progress", "color": "#FFF1C7", "hidden": false, "position": 1 }, { "object": "option", "id": "", "label": "Done", "color": "#CCF3D0", "hidden": false, "position": 2 } ], "position": 0, "created_at": "2026-09-06T10:00:00.000Z", "updated_at": "2026-09-06T10:00:00.000Z" } ] } ``` Keep `properties[0].id` (`` above) and the option ids — you'll need them next. ## 2. Add files to the board [#2-add-files-to-the-board] ```bash curl -X POST https://us.api.brault.app/v1/boards//files \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-6666-4c1a-9c1e-board-add-files" \ -d '{"file_ids": ["", ""]}' ``` ```json { "object": "board_files_batch", "board_id": "", "results": [ { "file_id": "", "status": "added" }, { "file_id": "", "status": "added" } ] } ``` If any id in `file_ids` is one your key can't reach, the **whole** batch fails before anything is added — there's no partial "added some, skipped one" outcome for a bad id. Re-sending the same ids is always safe: an id already on the board answers `already_in_board` rather than erroring. ## 3. Set a value [#3-set-a-value] ```bash curl -X PUT https://us.api.brault.app/v1/boards//files//properties/ \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": {"option_id": ""}}' ``` This returns the updated `board_file` — the file, this board's values for it, and when it was added. ## 4. Query the board by that value [#4-query-the-board-by-that-value] Filtering uses the same JSON grammar as `GET /v1/files?filter=…` (see [Conventions](/docs/conventions) for pagination and [the Boards reference](/docs/reference/boards) for the full operator table): ```bash curl -X POST https://us.api.brault.app/v1/boards//query \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filter": { "property": "", "status": { "equals": "" } }, "sort": "added_at", "order": "desc" }' ``` This is a read that uses `POST` because `filter` is a JSON document, not something that fits cleanly in a query string — sending an `Idempotency-Key` on it is harmless and just replays the same page. ## A note on file access [#a-note-on-file-access] Viewing a board is a read-through: if your key's creator can view the board, they see **every** file on it — including ones from libraries they hold no role in. Writing is narrower — adding, removing or setting a value for a file additionally requires the creator's role on **that file's own library or folder** to include the matching edit right. A board that aggregates files from libraries the creator can't edit is read-only for those specific files through the API, the same as it is in the web app. # Connect Make (/docs/guides/connect-make) Brault has no native Make app yet (it is on the roadmap). You do not need one: Make's webhook trigger receives Brault events, and its HTTP step calls any `/v1` route with your API key. This guide wires both directions. You need an API key with `webhooks:manage` (to register the endpoint) and `files:write` (for the import example). Create one under **Settings → Developers** in the app, see [Get an API key](/docs/get-an-api-key). ## 1. Trigger a Make workflow from a Brault event [#1-trigger-a-make-workflow-from-a-brault-event] Start the scenario with a **Webhooks → Custom webhook** module. Make gives you a unique `https://hook..make.com/…` URL; register it with Brault. Make learns the payload structure from the first request it receives, so run the scenario once in **Run once** mode and then send the test delivery below. ```bash curl -X POST https://us.api.brault.app/v1/webhooks \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-2222-4c1a-9c1e-make-hook" \ -d '{ "url": "", "name": "Make workflow", "events": ["file.created", "comment.created", "transfer.downloaded"] }' ``` Pick any tokens from the [event catalogue](/docs/webhooks#event-catalogue), or `["*"]` for everything. Then send a test delivery so Make has a sample payload to map fields from: ```bash curl -X POST https://us.api.brault.app/v1/webhooks//test \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` Every delivery body is the [event envelope](/docs/webhooks#the-event-envelope): `type`, `occurred_at`, `actor`, and a minimal `data` snapshot with the ids you need for the next step. Deliveries are at-least-once with no ordering guarantee, so deduplicate on `id` if a duplicate would matter to your workflow. Before production traffic, verify the `Brault-Signature` header: add a module that computes `HMAC-SHA256` of `.` with the secret returned when you created the endpoint and compare it with the `v1` value, following the steps on the [Webhooks](/docs/webhooks#verifying-the-signature) page. Route the scenario to stop when they differ. ## 2. Call the Brault API from Make [#2-call-the-brault-api-from-make] Use an **HTTP → Make a request** module. Add a header `Authorization` with value `Bearer bsk_…`, set **Body type** to `Raw` and **Content type** to `JSON (application/json)`. Map ids from the webhook bundle (for example `data.id` of a `file.created` event) into the request body. The example below imports a file that another step produced (any public `http(s)` URL) into a Brault library: ```http POST https://us.api.brault.app/v1/files/import Authorization: Bearer Content-Type: application/json Idempotency-Key: { "url": "https://example.com/exports/hero-v3.psd", "name": "hero-v3.psd", "library_id": "" } ``` The response is `202` with an `import` object: `status` starts at `queued`, and `file_id` is set once it reaches `succeeded`. Either poll `GET /v1/imports/` from a later step, or let Brault call you back: subscribe the endpoint from section 1 to `import.completed` and `import.failed` (a successful import also emits a normal `file.created`). Imports are capped at 5 GiB and 2 concurrent per brandspace. Anything else in the [reference](/docs/reference) works the same way: same base URL, same header, JSON in and out. Use `Idempotency-Key` on every `POST` that creates something, so a retried run does not create it twice. ## 3. Rate limits and errors [#3-rate-limits-and-errors] Make retries failed HTTP steps on its own schedule. Brault answers `429` with a `Retry-After` header when a bucket or the monthly quota is hit, and every error carries a `request_id` you can quote to support. See [Rate limits](/docs/plans-and-limits) and [Conventions](/docs/conventions#errors). # Connect n8n (/docs/guides/connect-n8n) Brault has no native n8n app yet (it is on the roadmap). You do not need one: n8n's webhook trigger receives Brault events, and its HTTP step calls any `/v1` route with your API key. This guide wires both directions. You need an API key with `webhooks:manage` (to register the endpoint) and `files:write` (for the import example). Create one under **Settings → Developers** in the app, see [Get an API key](/docs/get-an-api-key). ## 1. Trigger an n8n workflow from a Brault event [#1-trigger-an-n8n-workflow-from-a-brault-event] Start the workflow with a **Webhook** node set to `POST`. n8n shows a test URL and a production URL; register the production one with Brault (the test URL only listens while you are testing in the editor). Register that URL as a Brault webhook endpoint. The URL must be `https://` on port 443 (or 8443\) and publicly reachable, which is true for n8n Cloud and for a self-hosted n8n behind a public HTTPS domain. ```bash curl -X POST https://us.api.brault.app/v1/webhooks \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-2222-4c1a-9c1e-n8n-hook" \ -d '{ "url": "", "name": "n8n workflow", "events": ["file.created", "comment.created", "transfer.downloaded"] }' ``` Pick any tokens from the [event catalogue](/docs/webhooks#event-catalogue), or `["*"]` for everything. Then send a test delivery so n8n has a sample payload to map fields from: ```bash curl -X POST https://us.api.brault.app/v1/webhooks//test \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` Every delivery body is the [event envelope](/docs/webhooks#the-event-envelope): `type`, `occurred_at`, `actor`, and a minimal `data` snapshot with the ids you need for the next step. Deliveries are at-least-once with no ordering guarantee, so deduplicate on `id` if a duplicate would matter to your workflow. Before production traffic, verify the `Brault-Signature` header. In n8n that is a **Code** node placed right after the Webhook node, running the JavaScript verification from the [Webhooks](/docs/webhooks#verifying-the-signature) page against the raw body and the secret returned when you created the endpoint. Reject the execution when it does not match. ## 2. Call the Brault API from n8n [#2-call-the-brault-api-from-n8n] Use an **HTTP Request** node. Create a **Header Auth** credential with name `Authorization` and value `Bearer bsk_…`, and attach it to the node so the key never appears in the workflow JSON. Set the method and URL from the reference, body content type JSON, and map fields from earlier nodes with expressions. The example below imports a file that another step produced (any public `http(s)` URL) into a Brault library: ```http POST https://us.api.brault.app/v1/files/import Authorization: Bearer Content-Type: application/json Idempotency-Key: { "url": "https://example.com/exports/hero-v3.psd", "name": "hero-v3.psd", "library_id": "" } ``` The response is `202` with an `import` object: `status` starts at `queued`, and `file_id` is set once it reaches `succeeded`. Either poll `GET /v1/imports/` from a later step, or let Brault call you back: subscribe the endpoint from section 1 to `import.completed` and `import.failed` (a successful import also emits a normal `file.created`). Imports are capped at 5 GiB and 2 concurrent per brandspace. Anything else in the [reference](/docs/reference) works the same way: same base URL, same header, JSON in and out. Use `Idempotency-Key` on every `POST` that creates something, so a retried run does not create it twice. ## 3. Rate limits and errors [#3-rate-limits-and-errors] n8n retries failed HTTP steps on its own schedule. Brault answers `429` with a `Retry-After` header when a bucket or the monthly quota is hit, and every error carries a `request_id` you can quote to support. See [Rate limits](/docs/plans-and-limits) and [Conventions](/docs/conventions#errors). # Connect Zapier (/docs/guides/connect-zapier) Brault has no native Zapier app yet (it is on the roadmap). You do not need one: Zapier's webhook trigger receives Brault events, and its HTTP step calls any `/v1` route with your API key. This guide wires both directions. Webhooks by Zapier is a premium app on Zapier's side; check your Zapier plan before building on it. You need an API key with `webhooks:manage` (to register the endpoint) and `files:write` (for the import example). Create one under **Settings → Developers** in the app, see [Get an API key](/docs/get-an-api-key). ## 1. Trigger a Zapier workflow from a Brault event [#1-trigger-a-zapier-workflow-from-a-brault-event] Start the Zap with **Webhooks by Zapier → Catch Hook**. Zapier shows a unique `https://hooks.zapier.com/hooks/catch/…` URL; register it with Brault. Zapier needs a sample request before it lets you map fields, so register the endpoint and send the test delivery below, then click **Find new records** (or the equivalent test button) in the trigger step. ```bash curl -X POST https://us.api.brault.app/v1/webhooks \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-2222-4c1a-9c1e-zapier-hook" \ -d '{ "url": "", "name": "Zapier workflow", "events": ["file.created", "comment.created", "transfer.downloaded"] }' ``` Pick any tokens from the [event catalogue](/docs/webhooks#event-catalogue), or `["*"]` for everything. Then send a test delivery so Zapier has a sample payload to map fields from: ```bash curl -X POST https://us.api.brault.app/v1/webhooks//test \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` Every delivery body is the [event envelope](/docs/webhooks#the-event-envelope): `type`, `occurred_at`, `actor`, and a minimal `data` snapshot with the ids you need for the next step. Deliveries are at-least-once with no ordering guarantee, so deduplicate on `id` if a duplicate would matter to your workflow. Before production traffic, verify the `Brault-Signature` header: add a **Code by Zapier** step right after the trigger that runs the JavaScript verification from the [Webhooks](/docs/webhooks#verifying-the-signature) page. Catch Hook exposes the parsed body rather than the raw bytes by default, so switch the trigger to **Catch Raw Hook** when you verify signatures. Stop the Zap when the check fails. ## 2. Call the Brault API from Zapier [#2-call-the-brault-api-from-zapier] Use **Webhooks by Zapier → Custom Request** (or **POST**). Add a header `Authorization` with value `Bearer bsk_…`, set **Payload Type** to `json`, and fill **Data** with the request body. Map ids from the trigger step into the body. The example below imports a file that another step produced (any public `http(s)` URL) into a Brault library: ```http POST https://us.api.brault.app/v1/files/import Authorization: Bearer Content-Type: application/json Idempotency-Key: { "url": "https://example.com/exports/hero-v3.psd", "name": "hero-v3.psd", "library_id": "" } ``` The response is `202` with an `import` object: `status` starts at `queued`, and `file_id` is set once it reaches `succeeded`. Either poll `GET /v1/imports/` from a later step, or let Brault call you back: subscribe the endpoint from section 1 to `import.completed` and `import.failed` (a successful import also emits a normal `file.created`). Imports are capped at 5 GiB and 2 concurrent per brandspace. Anything else in the [reference](/docs/reference) works the same way: same base URL, same header, JSON in and out. Use `Idempotency-Key` on every `POST` that creates something, so a retried run does not create it twice. ## 3. Rate limits and errors [#3-rate-limits-and-errors] Zapier retries failed HTTP steps on its own schedule. Brault answers `429` with a `Retry-After` header when a bucket or the monthly quota is hit, and every error carries a `request_id` you can quote to support. See [Rate limits](/docs/plans-and-limits) and [Conventions](/docs/conventions#errors). # Integrate webhooks (/docs/guides/integrate-webhooks) Webhooks push events to your own HTTPS endpoint instead of you polling for changes. Everything here needs the `webhooks:manage` scope — there's no separate read-only scope for webhooks, since a delivery record already carries your endpoint's URL and response data. ## 1. Register an endpoint [#1-register-an-endpoint] ```bash curl -X POST https://us.api.brault.app/v1/webhooks \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-bbbb-4c1a-9c1e-webhook-create" \ -d '{ "url": "https://example.com/hooks/brault", "name": "Production sync", "events": ["file.created", "file.deleted", "board.file.added"] }' ``` ```json { "object": "webhook", "id": "", "url": "https://example.com/hooks/brault", "name": "Production sync", "events": ["file.created", "file.deleted", "board.file.added"], "status": "active", "disabled_reason": null, "secret": "whsec_…", "secret_last4": "a1b2", "secret_rolled_at": null, "consecutive_failures": 0, "last_delivery_at": null, "last_success_at": null, "created_at": "2026-09-06T10:00:00.000Z", "updated_at": "2026-09-06T10:00:00.000Z", "created_by_id": "" } ``` **`secret` is only ever in this response and in the response of `POST …/roll-secret`.** Store it immediately — every other read of this endpoint returns `secret: null` and only `secret_last4` for display. `events` accepts either a list of catalogue tokens or the single element `"*"` for everything, including events a future version of the catalogue adds. The endpoint URL must be `https://`, on port 443 (or an explicit `8443`), and must resolve to a public address — see the [Webhooks reference](/docs/reference/webhooks) for the full validation rules. Your receiver only needs to answer `2xx` quickly. Verifying the payload is genuinely from Brault, and the full event catalogue, are covered on the [Webhooks](/docs/webhooks) page — read that before wiring up production traffic. ## 2. Send a test delivery [#2-send-a-test-delivery] ```bash curl -X POST https://us.api.brault.app/v1/webhooks//test \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` This delivers a synthetic `webhook.test` event to your endpoint right away — one attempt, no retry, and it works even on a `paused` or `disabled` endpoint. It never touches the endpoint's failure counters, so testing can't accidentally trip auto-disable. ## 3. What to expect once it's live [#3-what-to-expect-once-its-live] * **At-least-once, no ordering guarantee.** Design for idempotent handling on your side — every event has a stable `id` you can deduplicate on. * **Six attempts per delivery**, roughly 15s, 1min, 5min, 30min and 2h apart (with jitter) after the first — about 2h36m of retry horizon for a receiver that's briefly down. * **Auto-disable** kicks in after 100 consecutive failed deliveries, or 24 hours without a single success while failures continue. A `PATCH` back to `"status": "active"` clears the counters and resumes delivery — nothing that happened while disabled or paused is replayed. * **Events caused by your own key's requests are delivered too** — unlike in-app notifications, which skip the actor's own actions. See [Webhooks](/docs/webhooks) for the full event catalogue and the signature verification snippets, and the [Webhooks reference](/docs/reference/webhooks) for every endpoint and delivery-record field. # Publish pages (/docs/guides/publish-pages) A page is a lightweight document — a brand guide, a one-off brief — built from structured blocks rather than freeform Markdown. Scopes needed: `pages:write` to create, append and publish; `pages:read` to read it back. ## 1. Create a page [#1-create-a-page] ```bash curl -X POST https://us.api.brault.app/v1/pages \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-9999-4c1a-9c1e-page-create" \ -d '{ "name": "Q4 Brand Brief", "folder_id": "", "blocks": [ { "type": "heading", "level": 1, "text": "Q4 Brand Brief" }, { "type": "paragraph", "text": "Everything the extended team needs for this quarter’s campaign." }, { "type": "bullet_list", "items": ["Primary palette locked", "New product shots due Nov 1"] } ] }' ``` The response is the page detail, including the stored content: ```json { "object": "page", "id": "", "name": "Q4 Brand Brief", "library_id": "", "folder_id": "", "published": false, "published_at": null, "published_by_id": null, "public_url": null, "created_at": "2026-09-06T10:00:00.000Z", "updated_at": "2026-09-06T10:00:00.000Z", "created_by_id": "", "trashed_at": null, "content": { "format": "json", "json": { "type": "doc", "content": [ /* … */ ] } } } ``` Available block types: `paragraph`, `heading` (levels 1–3), `bullet_list`, `ordered_list`, `code_block`, `quote`, `divider`. There's no inline formatting (bold, links), no images and no nesting yet — see the [Pages reference](/docs/reference/pages) for exact field limits. ## 2. Append more content later [#2-append-more-content-later] ```bash curl -X POST https://us.api.brault.app/v1/pages//blocks \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-aaaa-4c1a-9c1e-page-append" \ -d '{"blocks": [{"type": "paragraph", "text": "Update: shots pushed to Nov 8."}]}' ``` Always send an `Idempotency-Key` here — unlike most `/v1` writes, appending blocks is **not** naturally idempotent: a blind retry appends the same paragraph twice. ## 3. Publish it [#3-publish-it] ```bash curl -X POST https://us.api.brault.app/v1/pages//publish \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` The response's `public_url` (`https://brault.app/site/`) is now reachable by anyone who has the link — no account required. `POST …/unpublish` withdraws it again. ## Reading it back [#reading-it-back] ```bash curl "https://us.api.brault.app/v1/pages/?format=markdown" \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` `format=markdown` renders the same content as plain-text Markdown for the block types above — useful for feeding a page into something that expects text rather than the raw ProseMirror JSON. It never carries image sources, and it does not escape Markdown special characters in your prose, so treat it as a rendering convenience rather than a lossless export. # Review workflows (/docs/guides/review-workflows) Scopes needed: `comments:write` to comment, `comments:read` to list or export, `shares:write` to create a review link, and `members:read` to resolve a user id to a name. ## 1. Comment on a file [#1-comment-on-a-file] Comments (and replies) are nested under the file they belong to. A video or audio comment can anchor to a timestamp; an image or video comment can carry drawn annotations. ```bash curl -X POST https://us.api.brault.app/v1/files//comments \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-7777-4c1a-9c1e-comment-create" \ -d '{ "text": "The logo needs to sit lower in this frame.", "timestamp_ms": 4200, "annotations": [ { "type": "rectangle", "coordinates": [{"x": 0.62, "y": 0.08}, {"x": 0.94, "y": 0.22}] } ] }' ``` ```json { "object": "comment", "id": "", "file_id": "", "version_id": null, "text": "The logo needs to sit lower in this frame.", "author": { "user_id": "" }, "anonymous_name": null, "mentions": [], "timestamp_ms": 4200, "annotations": [ { "type": "rectangle", "coordinates": [{"x": 0.62, "y": 0.08}, {"x": 0.94, "y": 0.22}] } ], "resolved": false, "resolved_at": null, "resolved_by_id": null, "has_replies": false, "created_at": "2026-09-06T10:00:00.000Z", "updated_at": "2026-09-06T10:00:00.000Z", "edited_at": null } ``` Mention someone inline with `[@]` in `text` — resolve the id to a name with `GET /v1/members/:userId` under the `members:read` scope. ## 2. Resolve it once addressed [#2-resolve-it-once-addressed] ```bash curl -X POST https://us.api.brault.app/v1/files//comments//resolve \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` Anyone who can view the file can resolve or reopen a comment. Only the original author can edit its text — an owner or admin gets the same shared `404` as anyone else on someone else's comment. Deleting it is broader: the author, or a creator whose brandspace role is owner or admin, can delete it. ## 3. Share the file for external review [#3-share-the-file-for-external-review] `access: "review"` lets a visitor comment on the file without a Brault account: ```bash curl -X POST https://us.api.brault.app/v1/shared-links \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f14e45f-8888-4c1a-9c1e-share-create" \ -d '{ "target": { "type": "file", "id": "" }, "access": "review", "anonymous_comments": true, "expires_at": "2026-10-01T00:00:00.000Z" }' ``` ```json { "object": "shared_link", "id": "8xK3mZq2p1a", "url": "https://brault.app/shared/8xK3mZq2p1a", "target": { "type": "file", "id": "" }, "access": "review", "password_protected": false, "expires_at": "2026-10-01T00:00:00.000Z", "expired": false, "anonymous_comments": true, "show_versions": true, "show_properties": true, "display_mode": "grid", "board_views": null, "view_count": 0, "created_at": "2026-09-06T10:00:00.000Z", "updated_at": "2026-09-06T10:00:00.000Z", "created_by_id": "" } ``` Send `password` (4–72 **bytes**, bcrypt's own limit) to require a password, and remember that a target can only ever have one link — creating a second one for the same file answers `409 conflict` with the existing link's id in `details.shared_link_id`. **A target you can name isn't necessarily one you can already see everything in.** For `file`, `folder` and `library` targets, `shares:write` only checks that the target belongs to your own brandspace — not that your key's creator can otherwise open it. A `board` target is the one exception: sharing a board additionally requires the creator's own management access to it. ## 4. Export comments for a paper trail [#4-export-comments-for-a-paper-trail] ```bash curl "https://us.api.brault.app/v1/files//comments?format=csv" \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -o comments.csv ``` The export never carries a name or email — every person is a user id, except `anonymous_name`, the display name a shared-link visitor typed on their own comment. See the [Comments reference](/docs/reference/comments) for the full column list. # Send a transfer (/docs/guides/send-transfers) 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 [#1-send-files-that-are-already-in-brault] With no `uploads` declared, one call does everything: ```bash 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": "" }, { "id": "", "version_id": "" }], "folders": [""], "expires_in_days": 14, "password": "clientaccess2026" }' ``` ```json { "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": "" } ``` `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 [#2-send-files-you-upload-yourself] Declare them, and the transfer opens as a draft instead of finishing: ```bash 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": "" }], "uploads": [ { "name": "brief.pdf", "size": 1048576 }, { "name": "master.mov", "size": 3221225472 } ] }' ``` ```json { "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": "", "uploads": [ { "object": "transfer_upload", "id": "", "name": "brief.pdf", "size": 1048576, "method": "put", "url": "https://….s3.amazonaws.com/…", "part_size": null, "status": "pending" }, { "object": "transfer_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) [#a-single-put-up-to-256-mib] ```bash curl -X PUT "" --data-binary @brief.pdf curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/uploads//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](/docs/conventions)). ```json { "object": "transfer_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) [#multipart-above-256-mib] Ask for the part URLs, PUT each part, keep every `ETag` response header, then send them all: ```bash curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/uploads//parts \ -H "Authorization: Bearer $BRAULT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "part_numbers": [1, 2, 3] }' ``` ```json { "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/…" } ] } ``` ```bash curl -X POST https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL/uploads//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 [#finalize] ```bash 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 [#3-wait-for-the-zip] `status` moves `processing` → `ready` when the zip finishes, and `zip_url` appears with it. Two ways to find out: **Poll** `GET /v1/transfers/` 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](/docs/guides/integrate-webhooks); the four transfer events are `transfer.created`, `transfer.ready`, `transfer.downloaded` and `transfer.expired`. ```json { "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": "", "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 [#4-change-the-expiry-or-the-password-and-end-it-early] ```bash 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`. ```bash curl -X DELETE https://us.api.brault.app/v1/transfers/kR7mN2pQ5vL \ -H "Authorization: Bearer $BRAULT_API_KEY" ``` ```json { "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 [#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/` 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`. # Upload and organize files (/docs/guides/upload-and-organize) 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 [#1-create-a-library] A library is the top-level container (called a "workspace" internally — the API only ever calls it a library). ```bash 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"}' ``` ```json { "object": "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": "" } ``` ## 2. Create a folder [#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). ```bash 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": ""}' ``` ## 3. Upload a file [#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: ```bash 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": ""}' ``` ```json { "object": "upload", "id": "", "method": "put", "file_id": "", "version_id": null, "upload_url": "https://….s3.amazonaws.com/…", "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: ```bash curl -X PUT "" \ --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](/docs/reference/uploads) for the exact multipart shapes. Either way, finish with: ```bash curl -X POST https://us.api.brault.app/v1/uploads//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 [#4-list-whats-in-the-folder] ```bash curl "https://us.api.brault.app/v1/files?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](/docs/reference/files) for the complete parameter list, and [Automate boards](/docs/guides/automate-boards) for filtering by a property value instead of a native field. ## Cleaning up [#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. # API reference (/docs/reference) Each group below lists its operations; every operation page shows parameters, request and response schemas, and examples. See [Getting started](/docs) for keys and [Conventions](/docs/conventions) for pagination, errors and versioning. # Retrieve the brandspace this key belongs to (/docs/reference/brandspace/V1BrandspaceController_retrieve) GET /v1/brandspace Retrieve the brandspace this key belongs to Tag: Brandspace Available to every key, whatever its scopes. The id is always the key’s own. Responses: - 200: BrandspaceDto - default: V1ErrorDto # List members (/docs/reference/brandspace/V1MembersController_list) GET /v1/members List members Tag: Brandspace Members of the brandspace with their role. Includes email — that is what the members:read scope buys. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: MemberListDto - default: V1ErrorDto # List roles (/docs/reference/brandspace/V1MembersController_listRoles) GET /v1/roles List roles Tag: Brandspace Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: RoleListDto - default: V1ErrorDto # Retrieve a member (/docs/reference/brandspace/V1MembersController_retrieve) GET /v1/members/{userId} Retrieve a member Tag: Brandspace Parameters: - `userId` (path, required, string) Responses: - 200: MemberDto - default: V1ErrorDto # Brandspace (/docs/reference/brandspace) # Comment on a file (/docs/reference/comments/V1CommentsController_create) POST /v1/files/{fileId}/comments Comment on a file Tag: Comments Mention a member with the inline token [@]; the response lists the mentions that resolved. Parameters: - `fileId` (path, required, string) Request body: CreateCommentBodyDto (required) Required fields: text Responses: - 201: CommentDto - default: V1ErrorDto # List a file’s comments (/docs/reference/comments/V1CommentsController_list) GET /v1/files/{fileId}/comments List a file’s comments Tag: Comments Oldest first. Filter with resolved and version_id. format=csv answers text/csv with every matching comment and its replies, ignoring limit and cursor. A page (file_type canvas) is a valid file here; a page comment anchors to the page as a whole. Parameters: - `fileId` (path, required, string) - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. - `resolved` (query, optional, boolean) — true for resolved only, false for open only; absent for all. - `version_id` (query, optional, string) — Only comments anchored to that version of this file. - `format` (query, optional, string) — csv answers text/csv; limit and cursor are ignored. Responses: - 200: CommentListDto - default: V1ErrorDto # Delete a comment (/docs/reference/comments/V1CommentsController_remove) DELETE /v1/files/{fileId}/comments/{commentId} Delete a comment Tag: Comments The author, or an owner or admin of the brandspace. Permanent, and it deletes the comment’s replies. Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Reopen a comment (/docs/reference/comments/V1CommentsController_reopen) POST /v1/files/{fileId}/comments/{commentId}/reopen Reopen a comment Tag: Comments Clears resolved_by_id; a reopened comment carries no "reopened at". Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) Responses: - 200: CommentDto - default: V1ErrorDto # Resolve a comment (/docs/reference/comments/V1CommentsController_resolve) POST /v1/files/{fileId}/comments/{commentId}/resolve Resolve a comment Tag: Comments Anyone who can view the file. Idempotent in effect — send an `Idempotency-Key` to avoid a duplicate event and audit row. Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) Responses: - 200: CommentDto - default: V1ErrorDto # Retrieve a comment (/docs/reference/comments/V1CommentsController_retrieve) GET /v1/files/{fileId}/comments/{commentId} Retrieve a comment Tag: Comments Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) Responses: - 200: CommentDto - default: V1ErrorDto # Edit a comment’s text (/docs/reference/comments/V1CommentsController_update) PATCH /v1/files/{fileId}/comments/{commentId} Edit a comment’s text Tag: Comments The author only. Anyone else answers 404. Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) Request body: UpdateCommentBodyDto (required) Required fields: text Responses: - 200: CommentDto - default: V1ErrorDto # Reply to a comment (/docs/reference/comments/V1RepliesController_create) POST /v1/files/{fileId}/comments/{commentId}/replies Reply to a comment Tag: Comments Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) Request body: CreateReplyBodyDto (required) Required fields: text Responses: - 201: ReplyDto - default: V1ErrorDto # List a comment’s replies (/docs/reference/comments/V1RepliesController_list) GET /v1/files/{fileId}/comments/{commentId}/replies List a comment’s replies Tag: Comments Oldest first. Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: ReplyListDto - default: V1ErrorDto # Delete a reply (/docs/reference/comments/V1RepliesController_remove) DELETE /v1/files/{fileId}/comments/{commentId}/replies/{replyId} Delete a reply Tag: Comments The author, or an owner or admin of the brandspace. Permanent. Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) - `replyId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Edit a reply’s text (/docs/reference/comments/V1RepliesController_update) PATCH /v1/files/{fileId}/comments/{commentId}/replies/{replyId} Edit a reply’s text Tag: Comments The author only. Anyone else answers 404. Parameters: - `fileId` (path, required, string) - `commentId` (path, required, string) - `replyId` (path, required, string) Request body: UpdateReplyBodyDto (required) Required fields: text Responses: - 200: ReplyDto - default: V1ErrorDto # Comments (/docs/reference/comments) # Start a ZIP download (/docs/reference/downloads/V1DownloadsController_create) POST /v1/downloads Start a ZIP download Tag: Downloads Asynchronous. Poll /v1/downloads/{download_id} until `status` is `ready`, then use `url`. Request body: CreateDownloadBodyDto (required) Responses: - 202: DownloadDto - default: V1ErrorDto # Retrieve a ZIP download (/docs/reference/downloads/V1DownloadsController_retrieve) GET /v1/downloads/{downloadId} Retrieve a ZIP download Tag: Downloads Parameters: - `downloadId` (path, required, string) Responses: - 200: DownloadDto - default: V1ErrorDto # Downloads (/docs/reference/downloads) # List the boards a file is on (/docs/reference/files/V1FilesController_boards) GET /v1/files/{fileId}/boards List the boards a file is on Tag: Files Only the boards the key’s creator can see. A single page — a file sits on a handful of boards. Parameters: - `fileId` (path, required, string) Responses: - 200: FileBoardsListDto - default: V1ErrorDto # Copy a file (/docs/reference/files/V1FilesController_copy) POST /v1/files/{fileId}/copy Copy a file Tag: Files Give exactly one of `library_id`, `folder_id`, `to_root`. Parameters: - `fileId` (path, required, string) Request body: MoveBodyDto (required) Responses: - 201: FileDetailDto - default: V1ErrorDto # Get a download URL (/docs/reference/files/V1FilesController_download) GET /v1/files/{fileId}/download Get a download URL Tag: Files A signed URL for the file, valid 15 minutes. Counts against the plan’s daily download issuances. Parameters: - `fileId` (path, required, string) - `rendition` (query, optional, string) Responses: - 200: FileDownloadDto - default: V1ErrorDto # Move a file (/docs/reference/files/V1FilesController_move) POST /v1/files/{fileId}/move Move a file Tag: Files Give exactly one of `library_id`, `folder_id`, `to_root`. Parameters: - `fileId` (path, required, string) Request body: MoveBodyDto (required) Responses: - 200: FileDetailDto - default: V1ErrorDto # Trash or permanently delete a file (/docs/reference/files/V1FilesController_remove) DELETE /v1/files/{fileId} Trash or permanently delete a file Tag: Files Moves the file to the trash. `permanent=true` deletes the file and its versions irreversibly. Parameters: - `fileId` (path, required, string) - `permanent` (query, optional, boolean) Responses: - 200: DeletedDto - default: V1ErrorDto # Restore a trashed file (/docs/reference/files/V1FilesController_restore) POST /v1/files/{fileId}/restore Restore a trashed file Tag: Files Parameters: - `fileId` (path, required, string) Responses: - 200: FileDetailDto - default: V1ErrorDto # Retrieve a file (/docs/reference/files/V1FilesController_retrieve) GET /v1/files/{fileId} Retrieve a file Tag: Files Full metadata plus renditions signed for 15 minutes. The original file is NOT included here — request it from /v1/files/{fileId}/download, which counts against the download limit. Parameters: - `fileId` (path, required, string) Responses: - 200: FileDetailDto - default: V1ErrorDto # Set or clear a brandspace property value on a file (/docs/reference/files/V1FilesController_setProperty) PUT /v1/files/{fileId}/properties/{propertyId} Set or clear a brandspace property value on a file Tag: Files Send exactly one field, named after the property’s type. `null` clears a scalar; tag takes { add, remove }. Parameters: - `fileId` (path, required, string) - `propertyId` (path, required, string) Request body: SetBrandspaceValueBodyDto (required) Responses: - 200: FileDetailDto - default: V1ErrorDto # Find visually similar files (/docs/reference/files/V1FilesController_similar) GET /v1/files/{fileId}/similar Find visually similar files Tag: Files Nearest neighbours of this file’s embedding. Consumes the search rate bucket. Parameters: - `fileId` (path, required, string) - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: SimilarFilesDto - default: V1ErrorDto # Update a file (/docs/reference/files/V1FilesController_update) PATCH /v1/files/{fileId} Update a file Tag: Files Rename, replace the tag set, or point a link file at a new URL. Every field is optional. Parameters: - `fileId` (path, required, string) Request body: UpdateFileBodyDto (required) Responses: - 200: FileDetailDto - default: V1ErrorDto # List files (/docs/reference/files/V1FilesListController_list) GET /v1/files List files Tag: Files Files the key’s creator can see. Folders are never included. `q` runs the same keyword + natural-language search as the app and consumes the search rate bucket. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. - `library_id` (query, optional, string) - `folder_id` (query, optional, string) - `recursive` (query, optional, boolean) — Include files in every descendant folder. - `q` (query, optional, string) — Keyword and natural-language search. Consumes the search rate bucket. - `extensions` (query, optional, array of string) — Comma-separated extensions, without the dot. - `kind` (query, optional, array of string) - `created_after` (query, optional, string) - `created_before` (query, optional, string) - `uploaded_by` (query, optional, array of string) — Comma-separated user ids. - `min_size` (query, optional, number) — Bytes. - `max_size` (query, optional, number) — Bytes. - `filter` (query, optional, string) — A URL-encoded JSON filter document (boards.md § 5). A condition may name a brandspace property or a board property; a board-property condition matches files through their membership on that property’s board. Combines with every other parameter with AND — which is why a top-level `or` filter cannot be sent together with uploaded_by, extensions, kind, min_size, max_size, created_after or created_before (400 validation_error). - `sort` (query, optional, string) - `order` (query, optional, string) Responses: - 200: FileListDto - default: V1ErrorDto # Files (/docs/reference/files) # Create a folder (/docs/reference/folders/V1FoldersController_create) POST /v1/folders Create a folder Tag: Folders Request body: CreateFolderBodyDto (required) Required fields: name Responses: - 201: FolderDto - default: V1ErrorDto # List folders (/docs/reference/folders/V1FoldersController_list) GET /v1/folders List folders Tag: Folders The folder tree the key’s creator can see. Paginated in the API layer. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. - `library_id` (query, optional, string) — Restrict to one library. - `folder_id` (query, optional, string) — Restrict to the children of one folder. - `recursive` (query, optional, boolean) — Include every descendant, not only direct children. Responses: - 200: FolderListDto - default: V1ErrorDto # Move a folder (/docs/reference/folders/V1FoldersController_move) POST /v1/folders/{folderId}/move Move a folder Tag: Folders Give exactly one of `library_id`, `folder_id`. A folder always belongs to a library, so there is no move to the brandspace root. Parameters: - `folderId` (path, required, string) Request body: MoveFolderBodyDto (required) Responses: - 200: FolderDto - default: V1ErrorDto # Trash or permanently delete a folder (/docs/reference/folders/V1FoldersController_remove) DELETE /v1/folders/{folderId} Trash or permanently delete a folder Tag: Folders Moves the folder to the trash. `permanent=true` deletes it and its contents irreversibly. Parameters: - `folderId` (path, required, string) - `permanent` (query, optional, boolean) Responses: - 200: DeletedDto - default: V1ErrorDto # Restore a trashed folder (/docs/reference/folders/V1FoldersController_restore) POST /v1/folders/{folderId}/restore Restore a trashed folder Tag: Folders Parameters: - `folderId` (path, required, string) Responses: - 200: FolderDto - default: V1ErrorDto # Retrieve a folder (/docs/reference/folders/V1FoldersController_retrieve) GET /v1/folders/{folderId} Retrieve a folder Tag: Folders Parameters: - `folderId` (path, required, string) Responses: - 200: FolderDetailDto - default: V1ErrorDto # Rename or recolor a folder (/docs/reference/folders/V1FoldersController_update) PATCH /v1/folders/{folderId} Rename or recolor a folder Tag: Folders Parameters: - `folderId` (path, required, string) Request body: UpdateFolderBodyDto (required) Responses: - 200: FolderDto - default: V1ErrorDto # Folders (/docs/reference/folders) # Add files to a board (/docs/reference/boards/V1BoardFilesController_addFiles) POST /v1/boards/{boardId}/files Add files to a board Tag: Boards Every id is either added or already on the board. An id this key cannot serve fails the whole request with 404 before anything is added. Parameters: - `boardId` (path, required, string) Request body: AddBoardFilesBodyDto (required) Required fields: file_ids Responses: - 200: BoardFilesBatchDto - default: V1ErrorDto # Query the files on a board (/docs/reference/boards/V1BoardFilesController_query) POST /v1/boards/{boardId}/query Query the files on a board Tag: Boards A read expressed as POST because its filter is a JSON document. Returns board_file objects: the file, this board’s values for it, and when it was added. Trashed files are never listed. Parameters: - `boardId` (path, required, string) Request body: QueryBoardFilesBodyDto (required) Responses: - 200: BoardFileListDto - default: V1ErrorDto # Remove a file from a board (/docs/reference/boards/V1BoardFilesController_removeFile) DELETE /v1/boards/{boardId}/files/{fileId} Remove a file from a board Tag: Boards Removes the membership and this board’s values for the file. The file itself stays where it lives. A file that is not on the board answers 404. Parameters: - `boardId` (path, required, string) - `fileId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Set or clear a board property value (/docs/reference/boards/V1BoardFilesController_setValue) PUT /v1/boards/{boardId}/files/{fileId}/properties/{propertyId} Set or clear a board property value Tag: Boards Send exactly one field, named after the property’s type. `null` clears a scalar; multi_tag takes { add, remove }. Parameters: - `boardId` (path, required, string) - `fileId` (path, required, string) - `propertyId` (path, required, string) Request body: SetBoardValueBodyDto (required) Responses: - 200: BoardFileDto - default: V1ErrorDto # Create a board property (/docs/reference/boards/V1BoardPropertiesController_create) POST /v1/boards/{boardId}/properties Create a board property Tag: Boards Parameters: - `boardId` (path, required, string) Request body: CreateBoardPropertyBodyDto (required) Required fields: name, type Responses: - 201: PropertyDto - default: V1ErrorDto # Create an option (/docs/reference/boards/V1BoardPropertiesController_createOption) POST /v1/boards/{boardId}/properties/{propertyId}/options Create an option Tag: Boards `status` and `multi_tag` properties only. Parameters: - `boardId` (path, required, string) - `propertyId` (path, required, string) Request body: CreateOptionBodyDto (required) Required fields: label Responses: - 201: OptionDto - default: V1ErrorDto # List board properties (/docs/reference/boards/V1BoardPropertiesController_list) GET /v1/boards/{boardId}/properties List board properties Tag: Boards In position order, each with its options. Parameters: - `boardId` (path, required, string) - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: PropertyListDto - default: V1ErrorDto # Delete a board property (/docs/reference/boards/V1BoardPropertiesController_remove) DELETE /v1/boards/{boardId}/properties/{propertyId} Delete a board property Tag: Boards Its options and every value files hold for it go with it. Parameters: - `boardId` (path, required, string) - `propertyId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Delete an option (/docs/reference/boards/V1BoardPropertiesController_removeOption) DELETE /v1/boards/{boardId}/properties/{propertyId}/options/{optionId} Delete an option Tag: Boards Clears it from every file’s value: a status value pointing at it becomes unset, a multi_tag value loses that tag. Parameters: - `boardId` (path, required, string) - `propertyId` (path, required, string) - `optionId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a board property (/docs/reference/boards/V1BoardPropertiesController_retrieve) GET /v1/boards/{boardId}/properties/{propertyId} Retrieve a board property Tag: Boards Parameters: - `boardId` (path, required, string) - `propertyId` (path, required, string) Responses: - 200: PropertyDto - default: V1ErrorDto # Rename a board property (/docs/reference/boards/V1BoardPropertiesController_update) PATCH /v1/boards/{boardId}/properties/{propertyId} Rename a board property Tag: Boards A property’s type and position are fixed. Parameters: - `boardId` (path, required, string) - `propertyId` (path, required, string) Request body: UpdateBoardPropertyBodyDto (required) Required fields: name Responses: - 200: PropertyDto - default: V1ErrorDto # Update an option (/docs/reference/boards/V1BoardPropertiesController_updateOption) PATCH /v1/boards/{boardId}/properties/{propertyId}/options/{optionId} Update an option Tag: Boards Parameters: - `boardId` (path, required, string) - `propertyId` (path, required, string) - `optionId` (path, required, string) Request body: UpdateOptionBodyDto (required) Responses: - 200: OptionDto - default: V1ErrorDto # Create a board (/docs/reference/boards/V1BoardsController_create) POST /v1/boards Create a board Tag: Boards Request body: CreateBoardBodyDto (required) Required fields: name Responses: - 201: BoardDetailDto - default: V1ErrorDto # List boards (/docs/reference/boards/V1BoardsController_list) GET /v1/boards List boards Tag: Boards Boards of the brandspace the key’s creator can see: every board when their role carries boards.manage_all, otherwise only the boards they were granted. Pinned boards first, then the board’s own position. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: BoardListDto - default: V1ErrorDto # List board members (/docs/reference/boards/V1BoardsController_members) GET /v1/boards/{boardId}/members List board members Tag: Boards User ids only: everyone with an explicit grant plus everyone whose brandspace role carries boards.manage_all. Resolve a name or email with GET /v1/members/{userId} (members:read). Single page. Parameters: - `boardId` (path, required, string) Responses: - 200: BoardMemberListDto - default: V1ErrorDto # Delete a board (/docs/reference/boards/V1BoardsController_remove) DELETE /v1/boards/{boardId} Delete a board Tag: Boards Permanent: there is no trash for boards. The board’s properties, options, memberships and values go with it; the files themselves are untouched. Parameters: - `boardId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a board (/docs/reference/boards/V1BoardsController_retrieve) GET /v1/boards/{boardId} Retrieve a board Tag: Boards The board plus its properties and their options. Parameters: - `boardId` (path, required, string) Responses: - 200: BoardDetailDto - default: V1ErrorDto # Update a board (/docs/reference/boards/V1BoardsController_update) PATCH /v1/boards/{boardId} Update a board Tag: Boards name, color and emoji are applied first; pinned is applied after them. Only the fields sent change. Parameters: - `boardId` (path, required, string) Request body: UpdateBoardBodyDto (required) Responses: - 200: BoardDetailDto - default: V1ErrorDto # Boards (/docs/reference/boards) # Retrieve an import (/docs/reference/imports/V1ImportsController_retrieve) GET /v1/imports/{importId} Retrieve an import Tag: Imports Parameters: - `importId` (path, required, string) Responses: - 200: ImportDto - default: V1ErrorDto # Import a file from a URL (/docs/reference/imports/V1ImportsController_start) POST /v1/files/import Import a file from a URL Tag: Imports Downloads a public http(s) URL into the brandspace. Asynchronous: poll /v1/imports/{import_id}. Maximum 5 GiB, 2 concurrent imports per brandspace. Request body: StartImportBodyDto (required) Required fields: url Responses: - 202: ImportDto - default: V1ErrorDto # Imports (/docs/reference/imports) # Create a library (/docs/reference/libraries/V1LibrariesController_create) POST /v1/libraries Create a library Tag: Libraries Request body: CreateLibraryBodyDto (required) Required fields: name Responses: - 201: LibraryDto - default: V1ErrorDto # List libraries (/docs/reference/libraries/V1LibrariesController_list) GET /v1/libraries List libraries Tag: Libraries Libraries the key’s creator can see. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: LibraryListDto - default: V1ErrorDto # Delete a library (/docs/reference/libraries/V1LibrariesController_remove) DELETE /v1/libraries/{libraryId} Delete a library Tag: Libraries Parameters: - `libraryId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a library (/docs/reference/libraries/V1LibrariesController_retrieve) GET /v1/libraries/{libraryId} Retrieve a library Tag: Libraries Parameters: - `libraryId` (path, required, string) Responses: - 200: LibraryDetailDto - default: V1ErrorDto # Update a library (/docs/reference/libraries/V1LibrariesController_update) PATCH /v1/libraries/{libraryId} Update a library Tag: Libraries Only `name` and `description` are settable. Parameters: - `libraryId` (path, required, string) Request body: UpdateLibraryBodyDto (required) Responses: - 200: LibraryDto - default: V1ErrorDto # Libraries (/docs/reference/libraries) # The webhook event catalogue (/docs/reference/meta/MetaController_events) GET /v1/events The webhook event catalogue Tag: Meta Every event type a webhook endpoint can subscribe to, with its group, a one-line description and the `object` its payload’s `data` carries. Put these tokens in a webhook’s `events`, or the single element "*" to receive everything, including event types added later. Responses: - 200: EventTypeListDto - default: V1ErrorDto # Who am I (/docs/reference/meta/MetaController_me) GET /v1/me Who am I Tag: Meta The brandspace, key, creator, plan and limits behind the presented API key — and, because this route is central, where the brandspace lives and which host to dial for its content. There is no separate region endpoint. Responses: - 200: ApiKeyContextDto - default: V1ErrorDto # Current usage (/docs/reference/meta/MetaController_usage) GET /v1/usage Current usage Tag: Meta Counters for the current windows and the monthly quota of the brandspace. Responses: - 200: UsageDto - default: V1ErrorDto # Health (/docs/reference/meta/V1PublicController_health) GET /v1/health Health Tag: Meta Liveness of the public API. Responses: - 200: HealthDto - default: V1ErrorDto # Meta (/docs/reference/meta) # Append blocks to a page (/docs/reference/pages/V1PagesController_appendBlocks) POST /v1/pages/{pageId}/blocks Append blocks to a page Tag: Pages Appends at the end of the live document, through the collaboration server, so open editors receive it immediately. Not naturally idempotent — send an Idempotency-Key. Answers 503 if the server has not persisted within 5 seconds; re-read the page before retrying. A page whose collaborative document was never initialized (seeded, or created outside this API, and never opened in the editor) answers 409 page_not_initialized: open it once in the editor, then retry. Parameters: - `pageId` (path, required, string) Request body: AppendBlocksBodyDto (required) Required fields: blocks Responses: - 200: PageDetailDto - default: V1ErrorDto # Create a page (/docs/reference/pages/V1PagesController_create) POST /v1/pages Create a page Tag: Pages Give exactly one of library_id, folder_id, to_root. blocks becomes the page’s initial content. Request body: CreatePageBodyDto (required) Required fields: name Responses: - 201: PageDetailDto - default: V1ErrorDto # List pages (/docs/reference/pages/V1PagesController_list) GET /v1/pages List pages Tag: Pages Pages the key’s creator can see, newest first. Listed by location only: no q, no filter, no kind. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. - `library_id` (query, optional, string) - `folder_id` (query, optional, string) - `recursive` (query, optional, boolean) — Include pages in every descendant folder. Responses: - 200: PageListDto - default: V1ErrorDto # Publish a page (/docs/reference/pages/V1PagesController_publish) POST /v1/pages/{pageId}/publish Publish a page Tag: Pages Anyone holding the page’s public_url can read it. Idempotent in effect — send an `Idempotency-Key` to avoid a duplicate event and audit row. Parameters: - `pageId` (path, required, string) Responses: - 200: PageDetailDto - default: V1ErrorDto # Trash or permanently delete a page (/docs/reference/pages/V1PagesController_remove) DELETE /v1/pages/{pageId} Trash or permanently delete a page Tag: Pages Moves the page to the trash. permanent=true deletes it and its comments irreversibly. Parameters: - `pageId` (path, required, string) - `permanent` (query, optional, boolean) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a page (/docs/reference/pages/V1PagesController_retrieve) GET /v1/pages/{pageId} Retrieve a page Tag: Pages The page plus its content. format=json returns the stored ProseMirror document verbatim; format=markdown renders it. Content is the last stored snapshot: an edit in progress appears after its next save. Parameters: - `pageId` (path, required, string) - `format` (query, optional, string) Responses: - 200: PageDetailDto - default: V1ErrorDto # Unpublish a page (/docs/reference/pages/V1PagesController_unpublish) POST /v1/pages/{pageId}/unpublish Unpublish a page Tag: Pages Withdraws the public URL. Idempotent in effect — send an `Idempotency-Key` to avoid a duplicate event and audit row. Parameters: - `pageId` (path, required, string) Responses: - 200: PageDetailDto - default: V1ErrorDto # Pages (/docs/reference/pages) # Create a brandspace property (/docs/reference/properties/V1PropertiesController_create) POST /v1/properties Create a brandspace property Tag: Properties Request body: CreateBrandspacePropertyBodyDto (required) Required fields: name, type Responses: - 201: PropertyDto - default: V1ErrorDto # Create an option (/docs/reference/properties/V1PropertiesController_createOption) POST /v1/properties/{propertyId}/options Create an option Tag: Properties `tag` properties only. Parameters: - `propertyId` (path, required, string) Request body: CreateBrandspaceOptionBodyDto (required) Required fields: label Responses: - 201: OptionDto - default: V1ErrorDto # List brandspace properties (/docs/reference/properties/V1PropertiesController_list) GET /v1/properties List brandspace properties Tag: Properties In position order, each with its options. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: PropertyListDto - default: V1ErrorDto # Delete a brandspace property (/docs/reference/properties/V1PropertiesController_remove) DELETE /v1/properties/{propertyId} Delete a brandspace property Tag: Properties Its options and every value files hold for it go with it. Parameters: - `propertyId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Delete an option (/docs/reference/properties/V1PropertiesController_removeOption) DELETE /v1/properties/{propertyId}/options/{optionId} Delete an option Tag: Properties Parameters: - `propertyId` (path, required, string) - `optionId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a brandspace property (/docs/reference/properties/V1PropertiesController_retrieve) GET /v1/properties/{propertyId} Retrieve a brandspace property Tag: Properties Parameters: - `propertyId` (path, required, string) Responses: - 200: PropertyDto - default: V1ErrorDto # Update a brandspace property (/docs/reference/properties/V1PropertiesController_update) PATCH /v1/properties/{propertyId} Update a brandspace property Tag: Properties Parameters: - `propertyId` (path, required, string) Request body: UpdateBrandspacePropertyBodyDto (required) Responses: - 200: PropertyDto - default: V1ErrorDto # Update an option (/docs/reference/properties/V1PropertiesController_updateOption) PATCH /v1/properties/{propertyId}/options/{optionId} Update an option Tag: Properties Parameters: - `propertyId` (path, required, string) - `optionId` (path, required, string) Request body: UpdateBrandspaceOptionBodyDto (required) Responses: - 200: OptionDto - default: V1ErrorDto # Properties (/docs/reference/properties) # Search files and folders (/docs/reference/search/V1SearchController_search) GET /v1/search Search files and folders Tag: Search Keyword, semantic and natural-language search over everything the key’s creator can see. Consumes the search rate bucket on every call. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. - `q` (query, required, string) — Keyword and natural-language query. - `scope` (query, optional, string) — `brandspace`, `library:` or `folder:`. Responses: - 200: SearchResultsDto - default: V1ErrorDto # Search (/docs/reference/search) # Create a shared link (/docs/reference/shared-links/V1SharedLinksController_create) POST /v1/shared-links Create a shared link Tag: Shared links One call: target, access, password, expiry, anonymous comments and (for a board) the views. If a later step fails the link is deleted again and the failure is returned. Request body: CreateSharedLinkBodyDto (required) Required fields: target Responses: - 201: SharedLinkDto - default: V1ErrorDto # List shared links (/docs/reference/shared-links/V1SharedLinksController_list) GET /v1/shared-links List shared links Tag: Shared links The brandspace’s links, newest first. File-scoped links are not included — the internal listing excludes them. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: SharedLinkListDto - default: V1ErrorDto # Delete a shared link (/docs/reference/shared-links/V1SharedLinksController_remove) DELETE /v1/shared-links/{sharedLinkId} Delete a shared link Tag: Shared links Permanent. Visitors holding the URL lose access at once. Parameters: - `sharedLinkId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a shared link (/docs/reference/shared-links/V1SharedLinksController_retrieve) GET /v1/shared-links/{sharedLinkId} Retrieve a shared link Tag: Shared links Parameters: - `sharedLinkId` (path, required, string) Responses: - 200: SharedLinkDto - default: V1ErrorDto # Update a shared link (/docs/reference/shared-links/V1SharedLinksController_update) PATCH /v1/shared-links/{sharedLinkId} Update a shared link Tag: Shared links Fields are applied in a fixed order and the first failure stops the sequence — the fields already applied stay applied. Re-read the link to see its state. Parameters: - `sharedLinkId` (path, required, string) Request body: UpdateSharedLinkBodyDto (required) Responses: - 200: SharedLinkDto - default: V1ErrorDto # Shared links (/docs/reference/shared-links) # Finalize a transfer (/docs/reference/transfers/V1TransfersController_complete) POST /v1/transfers/{transferId}/complete Finalize a transfer Tag: Transfers Every declared upload must be completed first. The transfer’s link works from this moment. Parameters: - `transferId` (path, required, string) Responses: - 200: TransferDetailDto - default: V1ErrorDto # Finish one declared upload (/docs/reference/transfers/V1TransfersController_completeUpload) POST /v1/transfers/{transferId}/uploads/{uploadId}/complete Finish one declared upload Tag: Transfers Send `parts` for a multipart upload. For a single PUT the server checks the object itself. Parameters: - `transferId` (path, required, string) - `uploadId` (path, required, string) Request body: CompleteUploadBodyDto (required) Responses: - 200: TransferUploadStatusDto - default: V1ErrorDto # Create a transfer (/docs/reference/transfers/V1TransfersController_create) POST /v1/transfers Create a transfer Tag: Transfers Send Brault files and folders to finish in one call, and/or declare uploads to finish later with the upload routes. Send at least one of files, folders or uploads. Request body: CreateTransferBodyDto (required) Responses: - 201: TransferCreatedDto - default: V1ErrorDto # List transfers (/docs/reference/transfers/V1TransfersController_list) GET /v1/transfers List transfers Tag: Transfers The brandspace’s transfers, newest first, with any open drafts interleaved by creation time. Parameters: - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: TransferListDto - default: V1ErrorDto # Get presigned URLs for multipart parts (/docs/reference/transfers/V1TransfersController_parts) POST /v1/transfers/{transferId}/uploads/{uploadId}/parts Get presigned URLs for multipart parts Tag: Transfers Parameters: - `transferId` (path, required, string) - `uploadId` (path, required, string) Request body: UploadPartsBodyDto (required) Required fields: part_numbers Responses: - 200: UploadPartsDto - default: V1ErrorDto # Expire a transfer (/docs/reference/transfers/V1TransfersController_remove) DELETE /v1/transfers/{transferId} Expire a transfer Tag: Transfers Recipients lose access at once. An open draft is canceled and its uploads released instead. Parameters: - `transferId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Retrieve a transfer (/docs/reference/transfers/V1TransfersController_retrieve) GET /v1/transfers/{transferId} Retrieve a transfer Tag: Transfers With its files and folders, and a fresh zip URL. Parameters: - `transferId` (path, required, string) Responses: - 200: TransferDetailDto - default: V1ErrorDto # Update a transfer (/docs/reference/transfers/V1TransfersController_update) PATCH /v1/transfers/{transferId} Update a transfer Tag: Transfers Set or clear the password, and/or move the expiry. Expiry days count from today. Parameters: - `transferId` (path, required, string) Request body: UpdateTransferBodyDto (required) Responses: - 200: TransferDto - default: V1ErrorDto # Transfers (/docs/reference/transfers) # Abort an upload (/docs/reference/uploads/V1UploadsController_abort) POST /v1/uploads/{uploadId}/abort Abort an upload Tag: Uploads Parameters: - `uploadId` (path, required, string) Responses: - 200: AbortedDto - default: V1ErrorDto # Finish an upload (/docs/reference/uploads/V1UploadsController_complete) POST /v1/uploads/{uploadId}/complete Finish an upload Tag: Uploads Creates the file (or the version) and starts processing. The file’s `status` is `processing`. Parameters: - `uploadId` (path, required, string) Request body: CompleteUploadBodyDto (required) Responses: - 201: FileDetailDto - default: V1ErrorDto # Get presigned URLs for multipart parts (/docs/reference/uploads/V1UploadsController_parts) POST /v1/uploads/{uploadId}/parts Get presigned URLs for multipart parts Tag: Uploads Parameters: - `uploadId` (path, required, string) Request body: UploadPartsBodyDto (required) Required fields: part_numbers Responses: - 200: UploadPartsDto - default: V1ErrorDto # Start an upload (/docs/reference/uploads/V1UploadsController_start) POST /v1/uploads Start an upload Tag: Uploads Returns a presigned URL (or a multipart session above 256 MiB). PUT the bytes to S3 yourself, then call complete. Send `file_id` to add a version to an existing file. Request body: StartUploadBodyDto (required) Required fields: name, size Responses: - 201: UploadDto - default: V1ErrorDto # Uploads (/docs/reference/uploads) # Make a version the active one (/docs/reference/versions/V1VersionsController_activate) POST /v1/files/{fileId}/versions/{versionId}/activate Make a version the active one Tag: Versions Parameters: - `fileId` (path, required, string) - `versionId` (path, required, string) Responses: - 200: VersionDto - default: V1ErrorDto # List a file’s versions (/docs/reference/versions/V1VersionsController_list) GET /v1/files/{fileId}/versions List a file’s versions Tag: Versions Newest first. Parameters: - `fileId` (path, required, string) - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. Responses: - 200: VersionListDto - default: V1ErrorDto # Delete a version (/docs/reference/versions/V1VersionsController_remove) DELETE /v1/files/{fileId}/versions/{versionId} Delete a version Tag: Versions Irreversible: the version’s bytes and every comment attached to it are deleted. This is not a trash operation. Parameters: - `fileId` (path, required, string) - `versionId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Versions (/docs/reference/versions) # Create a webhook endpoint (/docs/reference/webhooks/V1WebhooksController_create) POST /v1/webhooks Create a webhook endpoint Tag: Webhooks The response carries the signing secret. It is shown here and once more on roll-secret, and can never be read back. Sending an Idempotency-Key caches this response — the secret included — for 24 h; omit the header if you do not want that. Request body: CreateWebhookBodyDto (required) Required fields: url, events Responses: - 201: WebhookDto - default: V1ErrorDto # List an endpoint’s deliveries (/docs/reference/webhooks/V1WebhooksController_deliveries) GET /v1/webhooks/{webhookId}/deliveries List an endpoint’s deliveries Tag: Webhooks Newest first, optionally filtered by status. The cursor is opaque; do not construct one. At most the last 10,000 deliveries of an endpoint are kept, and none older than 30 days. Parameters: - `webhookId` (path, required, string) - `limit` (query, optional, number) - `cursor` (query, optional, string) — Opaque cursor from a previous response. - `status` (query, optional, string) Responses: - 200: DeliveryListDto - default: V1ErrorDto # Retrieve one delivery (/docs/reference/webhooks/V1WebhooksController_delivery) GET /v1/webhooks/{webhookId}/deliveries/{deliveryId} Retrieve one delivery Tag: Webhooks Parameters: - `webhookId` (path, required, string) - `deliveryId` (path, required, string) Responses: - 200: DeliveryDto - default: V1ErrorDto # List webhook endpoints (/docs/reference/webhooks/V1WebhooksController_list) GET /v1/webhooks List webhook endpoints Tag: Webhooks Every endpoint of the brandspace, newest first. `secret` is null on every row. Responses: - 200: WebhookListDto - default: V1ErrorDto # Redeliver one delivery (/docs/reference/webhooks/V1WebhooksController_redeliver) POST /v1/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver Redeliver one delivery Tag: Webhooks Queues a NEW delivery for the same event: attempt restarts at 1, origin is "redeliver" and replayed_from_id names the row it came from. The source row is not touched. Brault-Event-Id is unchanged, so a receiver deduplicating on it — as this API has always asked — sees a duplicate and drops it. Allowed only when the source delivery is success or exhausted, is not a webhook.test row, the endpoint is active, and the event payload is still inside the 30-day window. Parameters: - `webhookId` (path, required, string) - `deliveryId` (path, required, string) Responses: - 201: DeliveryDto - default: V1ErrorDto # Delete a webhook endpoint (/docs/reference/webhooks/V1WebhooksController_remove) DELETE /v1/webhooks/{webhookId} Delete a webhook endpoint Tag: Webhooks Soft delete. Pending deliveries are cancelled and the endpoint stops receiving events at once. Parameters: - `webhookId` (path, required, string) Responses: - 200: DeletedDto - default: V1ErrorDto # Replay a window of events (/docs/reference/webhooks/V1WebhooksController_replay) POST /v1/webhooks/{webhookId}/replays Replay a window of events Tag: Webhooks Queues one pending delivery per event of the brandspace that occurred in [from, to) and whose type this endpoint subscribes to now, skipping events it already delivered successfully unless include_delivered is true. The window is at most 7 days and must lie inside the 30-day event window; a call may match at most 10,000 events. Both bounds are refusals, never truncations, so a replay is never silently partial. webhook.test events are never replayed. Sending an Idempotency-Key caches this report for 24 h without replaying twice. Parameters: - `webhookId` (path, required, string) Request body: ReplayBodyDto (required) Required fields: from, to Responses: - 201: ReplayDto - default: V1ErrorDto # Retrieve a webhook endpoint (/docs/reference/webhooks/V1WebhooksController_retrieve) GET /v1/webhooks/{webhookId} Retrieve a webhook endpoint Tag: Webhooks Parameters: - `webhookId` (path, required, string) Responses: - 200: WebhookDto - default: V1ErrorDto # Roll the signing secret (/docs/reference/webhooks/V1WebhooksController_rollSecret) POST /v1/webhooks/{webhookId}/roll-secret Roll the signing secret Tag: Webhooks Returns a new secret and keeps the previous one valid for 24 h, during which every delivery carries two v1 signature entries, current first. A second roll inside that window is a 409 with details.retry_after. Sending an Idempotency-Key caches this response — the secret included — for 24 h. Parameters: - `webhookId` (path, required, string) Responses: - 200: WebhookDto - default: V1ErrorDto # Send a test delivery (/docs/reference/webhooks/V1WebhooksController_test) POST /v1/webhooks/{webhookId}/test Send a test delivery Tag: Webhooks Delivers a synthetic webhook.test event inline and returns the resulting delivery. One attempt, no retry, no effect on the endpoint’s failure counters, and it works on any status including disabled. Worst-case latency is the 5 s attempt timeout plus DNS. A failed test is a 200 with a delivery whose status is exhausted, not an error. DO NOT send an Idempotency-Key: a repeat inside 24 h would replay the first attempt’s delivery without sending anything. Parameters: - `webhookId` (path, required, string) Responses: - 200: DeliveryDto - default: V1ErrorDto # Update a webhook endpoint (/docs/reference/webhooks/V1WebhooksController_update) PATCH /v1/webhooks/{webhookId} Update a webhook endpoint Tag: Webhooks Send any of url, name, events or status. `events` replaces the list. Setting status to disabled is a 409: only the system disables an endpoint. Parameters: - `webhookId` (path, required, string) Request body: UpdateWebhookBodyDto (required) Responses: - 200: WebhookDto - default: V1ErrorDto # Webhooks (/docs/reference/webhooks)