Database endpoints
Every shared database is also a URL. Read it as JSON, CSV or a calendar feed, and post a row back with a plain HTML form.
One URL per database
When you share a page, a view, a doc or a site, the databases that share displays get an address of their own. It is the share’s own link plus the database id:
GET|POST /s/<shareId>/data/<databaseId>[.csv|.ics]It works on the platform host, on your workspace subdomain and on a custom domain you have pointed here — the same path on whichever host the visitor is already on. Inside a Brain-published site you can therefore use it as a relative link, and it keeps working after the site moves to its own domain.
This is the endpoint to reach for from a Brain-published page: a site page, a share, an email you send from one. It is not a general-purpose workspace API — for an application of your own, connect over MCP or use an s16_ API token instead.
JSON
/data/<databaseId>Returns { database: { id, name, properties }, rows, nextCursor }. Every row is flat: an id plus one key per property id.
CSV
/data/<databaseId>.csvA spreadsheet download (RFC 4180) with property NAMES as the header row, so it opens in Excel, Numbers or Sheets as-is.
Calendar
/data/<databaseId>.icsA calendar subscription — one VEVENT per row that has a start date. Link it and a visitor adds your schedule to their own calendar.
Reading rows
A row is flat and keyed by property id, with the display names in database.properties so you can label them. The CSV form inverts that and uses names as its header row. Read the ids from the response rather than hard-coding them; renaming a property in Brain never changes its id.
curl "https://team-brain.com/s/SHARE_ID/data/DATABASE_UUID?limit=100&sort=Name:asc"
{
"database": {
"id": "DATABASE_UUID",
"name": "Sessions",
"properties": [
{ "id": "p_title", "name": "Name", "type": "title" },
{ "id": "p_start", "name": "Starts", "type": "date" }
]
},
"rows": [
{ "id": "row_1", "p_title": "Opening keynote", "p_start": "2026-09-18T09:00:00.000Z" }
],
"nextCursor": null
}Query parameters
limit=<number>Rows per request. Default 100, maximum 2000.cursor=<number>Row offset. Pass back the nextCursor from the previous response.search=<text>Free-text match across the row.searchField=<property>Restrict search to one property (id or name).fields=<id,id>Comma-separated property ids to return. Everything else is omitted.sort=<property:asc|desc>Repeatable. Applied in the order given.filter=<property:operator:value>Repeatable, ANDed. The value keeps any colons it contains.sort and filter may each appear more than once. A property is named by id or by name in every parameter that takes one.
Calendar-only parameters (.ics)
title=<property>Event summary. Defaults to the title property.start=<property>Event start. Defaults to the first date property; a row without one is skipped.end=<property>Event end. Omitted when unset.location=<property>Event location.description=<property>Event description.Because these are real URLs, the useful thing to do with them is usually to link to them. A link works with no JavaScript, in a mail client, and inside an in-app browser — and it never has to be regenerated when a row changes.
<a href="/s/SHARE_ID/data/DATABASE_UUID.ics?title=Name&start=Starts&location=Venue">
Add every session to your calendar
</a>
<a href="/s/SHARE_ID/data/DATABASE_UUID.csv?limit=2000">
Download the list (CSV)
</a>Submitting a row
POST to the same path creates one row. It takes a native form post and a fetch with a JSON body alike, and field names may be property ids or names, so <input name="Email"> works without looking up a uuid.
<form method="post" action="/s/SHARE_ID/data/DATABASE_UUID">
<input name="Name" required>
<input name="Email" type="email" required>
<input type="hidden" name="_redirect" value="/thanks">
<button>Join</button>
</form>The write contract
The form is the permission. Writing is allowed because the shared page contains a form whose action names this database. A link to the same path is a read and grants nothing, and there is no separate switch to turn on.
_redirect is optional and only meaningful for a native form post: it sends the visitor to that page afterwards, so a refresh does not resubmit. It must be a path on this site; anything else is ignored.
Without it the answer is 201 with { id, values }. A refusal is 403, or 429 when rate limited, and always carries a readable error.
Writable property types are Title, Text, Number, Date, Checkbox, URL, Email, Phone, Select, Multi-select, Status. A locked database, a locked property, an unknown field name or a type outside that list refuses the whole row rather than saving part of it.
Unrecognised field names are dropped before the row is built, so an extra hidden input in your form is harmless.
Limits and caching
What the endpoint will and will not do
Reads: 600 requests per 5 minutes, counted per visitor per share. Over that, the answer is a 429 that says so — never a silently empty page of rows.
Writes: 30 per 5 minutes, counted the same way.
Page size: 100 rows by default, 2000 at most. Follow nextCursor for the rest; it is null on the last page.
Caching: a read answers with Cache-Control: public, max-age=10, s-maxage=10, stale-while-revalidate=60. 10 seconds is deliberately short so the page stays live. What reuses it is the visitor’s own browser, and any CDN you have put in front of your domain — Brain’s edge does not cache this, so a page that reads once per visit costs one request per visit.
Cross-origin: every response carries Access-Control-Allow-Origin: *, so a page on any origin can fetch it. Nothing here reads a cookie, which is exactly why that is safe.
Last updated August 26, 2026