Contact Focal team
Focal API
Focal API alpha
Section titled “Focal API alpha”Programmatic access to your Focal organization’s media. Bearer-token authenticated and scoped per organization.
Authentication
Send your token in the Authorization header:
Authorization: Bearer fat_<your token>
Tokens are minted by org admins at app.focal.inc → Settings → Organization → API tokens. The plaintext token is shown exactly once — store it somewhere safe.
Scopes
Each token carries one or more scopes. The minimum scope needed is listed on every endpoint below.
| Scope | Grants |
|---|---|
all:read |
Every *:read (covers media:read, future boards:read, etc.) |
all:write |
Every scope (read + write across all resources) |
media:read |
List and fetch media, and browse library folders |
media:write |
Update writable fields on media |
media:delete |
Soft-delete media |
boards:read |
List and fetch boards (and their rows and columns) |
boards:write |
Create and update board rows |
boards:delete |
Archive board rows |
collections:read |
List and fetch collections |
collections:write |
Create and update collections |
collections:delete |
Archive collections |
comments:read |
List and fetch comments on media and board rows |
comments:write |
Post, edit and react to comments as the connected user |
comments:delete |
Archive your own comments |
spaces:read |
List and fetch spaces, and a space’s users |
spaces:write |
Change a space’s settings — today the media library’s custom fields (attribute definitions), which reach every media in the space. Distinct from media:write, which writes attribute values; adding options to an existing tags field also rides on media:write, unless the field has allow_create: false, which reserves that to this scope. Does not grant creating or deleting spaces |
Space scoping
Tokens can additionally be restricted to a subset of spaces in
your organization. When the admin chose specific spaces during
creation, the token only sees media (and other resources) in
those spaces — anything outside the allow-list returns 404,
matching the same not-found shape as cross-org access.
Use GET /v1/account to inspect your token’s allow-list at any time.
space_id is required on list endpoints
Every list endpoint whose resource lives inside a space (GET /v1/media, GET /v1/boards, and more) requires a space_id
query param naming exactly one space:
?space_id=<uuid>
There is no implicit default — omitting it returns 400 bad_request. The API reads one space at a time: comma-separated
lists and wildcards are not accepted, so every query is explicitly
scoped and auditable. To cover several spaces, issue one request
per space (GET /v1/spaces lists the spaces your token can see).
Endpoints addressed by a resource id (GET /v1/media/{id},
GET /v1/folders/{id}, …) take no space_id — the resource
itself pins the space, which is enforced against your token’s
scope either way.
Pagination
List endpoints use cursor pagination:
- Call without a cursor to get page 1
- The response includes
next_cursor(ornullif you’re on the last page) andhas_more(a boolean for convenience) - Pass
next_cursorback as?cursor=<value>to fetch the next page
Cursors are opaque strings — don’t construct them by hand. They stay stable across writes, so paging through a busy dataset never skips or duplicates rows.
Selecting response fields
Every read endpoint accepts fields — a list of the properties you
want back. Everything else is omitted, so you can trim a wide payload
down to what you’re actually using:
GET /v1/media?space_id=<uuid>&fields=name,status,html_url
On the POST reads (/v1/media/batch, /v1/media/search) it
travels in the JSON body as an array instead:
{ "fields": ["name", "status"] }.
Three rules worth knowing:
idalways comes back, whether or not you name it — so a narrowed response is still something you can act on.- An unrecognised name is a
400, not a silent no-op. A typo that quietly returned the full object would be worse than an error. - Omitting
fieldsreturns the complete object, so existing integrations are unaffected.
Each endpoint’s fields parameter documents the exact set it
accepts, which is the endpoint’s own response shape.
Board rows: selecting columns
A board row’s cell values arrive as a fields array — one entry
per column on the board — which is the bulk of a wide board’s payload.
The row endpoints take a second parameter, columns, that narrows
that array:
GET /v1/boards/{id}/rows?columns=Status,Owner
Entries are column ids or column names (case-insensitive), and each
row’s fields come back in the order you list them — so you know
the shape before you parse it. A name that matches no column on the
board is a 400. On a view’s rows feed, columns narrows within the
view’s visible columns — a column the view hides isn’t selectable
through it.
Without columns, fields arrives in the board’s column order,
which is the order GET /v1/boards/{id}/columns lists columns in
(newest first). A board has no left-to-right order of its own — don’t
sort by Column.order, a legacy field nothing maintains (usually
null). Presentation order belongs to a view: read
visible_column_ids on GET /v1/boards/{id}/views for the order a
board is shown in.
The two parameters are orthogonal: fields picks the row’s top-level
properties (title, position, fields, …), columns picks
what goes inside fields.
Field naming
All request and response field names use snake_case. Date fields
are ISO 8601 strings.
media_type vs mime_type
What earlier drafts of this spec called mime_type is now
exposed as media_type. The rename tracks the
IANA terminology,
which retired “MIME type” in favour of “media type” — the value
itself (image/jpeg, video/mp4, …) is unchanged. If you
integrated against an earlier alpha build, swap the field name in
your client; the contents are identical.
Don’t confuse media_type with the top-level type field on
Media — type is Focal’s coarse asset bucket
(image/video/playable/file), while media_type
is the precise IANA identifier ad platforms expect.
Rate limiting
Every authenticated response includes the current quota state:
RateLimit-Limit: 1000 ← total budget per minute
RateLimit-Remaining: 847 ← units left in the current window
RateLimit-Reset: 42 ← seconds until the window resets
Each request charges a cost against the budget. Cheap operations (single fetches, identity lookups) cost 1; list endpoints cost 5; future write endpoints will cost 10. The exact cost is fixed per route — see each operation’s description.
When the budget is exhausted the next request returns 429 Too Many Requests with error.code = "rate_limited" and a
Retry-After: <seconds> header. Wait that many seconds before
retrying. Quotas are per-token, so minting additional tokens for
different workloads is a valid way to scale.
Reporting issues
Every response includes an X-Request-Id header — a UUID that
uniquely identifies the request in our logs. Error responses also
include it as error.request_id in the body. When reporting a
problem to support, include this id and we can look up exactly what
happened.
Authentication
Section titled “Authentication”bearerAuth
Section titled “bearerAuth”Send your API token in the Authorization header: Authorization: Bearer fat_<your token>. Org admins create tokens in Settings → Organization → API tokens.
Security scheme type: http
Bearer format: fat_