API

The BrainSTEM REST API gives programmatic access to the complete data model: every record you can see in the web interface can be read, created, changed and deleted through HTTPS endpoints that return JSON. API tools exist for Python and MATLAB, AI assistants can connect through the MCP server or the agent skills, and the server describes itself in a machine-readable reference that is generated from its code.

Table of contents

  1. Quick start
  2. Authentication
  3. Endpoint structure
  4. Available endpoints
  5. Public vs private portals
  6. HTTP methods
  7. Responses
  8. Query parameters
  9. Writing records
  10. Other endpoints
  11. Error handling
  12. Rate limits

Quick start

  1. Create a token on your API token page (see Authentication) and copy it.
  2. Install the Python client and put the token in the environment:

    pip install brainstem_python_api_tools
    export BRAINSTEM_API_TOKEN="<token>"
    
  3. List your sessions and create one:

    from brainstem_api_tools import BrainstemClient
    
    client = BrainstemClient()                      # reads BRAINSTEM_API_TOKEN
    sessions = client.load("session").json()["sessions"]
    print(len(sessions), "sessions on the first page")
    
    created = client.save("session", data={
        "name": "Example session",
        "projects": ["<project-id>"],
        "session_type": "subject",
        "subjects": ["<subject-id>"],
        "date_time": "2026-03-02T10:00:00Z",
    }).json()
    print(created["session"]["id"])
    

The same read and write with curl:

curl -H "Authorization: Bearer $BRAINSTEM_API_TOKEN" \
     https://www.brainstem.org/api/private/stem/session/

curl -X POST \
     -H "Authorization: Bearer $BRAINSTEM_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"name": "Example session", "projects": ["<project-id>"], "session_type": "subject", "subjects": ["<subject-id>"], "date_time": "2026-03-02T10:00:00Z"}' \
     https://www.brainstem.org/api/private/stem/session/

Replace <project-id> and <subject-id> with the ids of entries you can change; open the entry in the web interface and copy the id from the address bar or the API button (Finding an ID). A new session is recorded as performed unless you send "status": "planned". The required fields of every resource are on its page under Available endpoints.

Authentication

BrainSTEM uses Personal Access Tokens (PATs) for API authentication. Because BrainSTEM enforces two-factor authentication (2FA), tokens are issued through the browser only: your password is never typed into a script or a command line tool.

Token typeLifetimeHow to obtainBest for
Personal Access TokenOne year from creation. Using the token does not extend it; create a new one before it expires.Web UI, or the device authorization flow built into the Python and MATLAB toolsScripts, integrations, CLI tools, AI assistants

A user can hold up to 10 active tokens. Each token has a name, records when and from where it was last used, and can be revoked at any time. Revocation takes effect immediately. Checking a token shows when it expires.

Option 1: Create a token in the web UI

Open your API token management page (Profile → Security & access → API tokens), click Create New Personal Access Token, enter a descriptive name such as “Analysis pipeline” and confirm. You are asked to confirm your password and 2FA code first. Copy the token right away; it is shown only once.

Use it as a bearer token:

curl -H "Authorization: Bearer $BRAINSTEM_API_TOKEN" \
     https://www.brainstem.org/api/private/stem/project/

A token has no scopes: whoever holds it can read, change, delete and publish with all your rights until it expires a year later or you revoke it. Keep it out of shared code and notebooks, for example in the BRAINSTEM_API_TOKEN environment variable that all BrainSTEM tools read, and revoke it on your token page if it leaks.

Option 2: Device authorization flow

The Python and MATLAB tools and the brainstem command line sign you in through the browser. The tool asks the server for a device session, opens the approval page, and polls until you approve. The approval page requires your normal login including 2FA.

In a headless environment (a server or a container without a browser) the tool prints the approval URL and a short code; open the URL on any device, enter the code and approve.

A token obtained this way appears on your token page with the client name and a timestamp, has the same one-year lifetime, and can be revoked like any other. The requests behind the flow are described under Device authorization protocol, for authors of other clients.

Checking a token

GET /api/auth/token/info/ with the bearer header returns the token’s user, type, issue and expiry times. It answers 401 when the token is invalid or revoked.

Authentication in the Python and MATLAB tools

  • Python: BrainstemClient() reads BRAINSTEM_API_TOKEN, then a cached token, and otherwise opens the browser; BrainstemClient(headless=True) prints the code instead. See the Python API tool.
  • MATLAB: BrainstemClient() reads BRAINSTEM_API_TOKEN, then a cached token, and otherwise opens the browser. See the MATLAB API tool.
  • Web API tool: uses your browser session; no token needed. See the Web API tool.
  • MCP and agent skills: configure the bearer header once; see MCP server and Agent skills.

Both clients and the NWB exporter also read BRAINSTEM_API_URL to talk to another server than https://www.brainstem.org.

Security best practices

  • Never type your password into a script. Use the device flow or create a token in the web UI.
  • Keep tokens out of version control, logs and shared notebooks.
  • Give each script, machine and AI application its own named token so you can revoke one without touching the others.
  • Delete unused tokens on your token management page.

Device authorization protocol

The requests the clients make during the device authorization flow:

# Step 1 — the tool starts a device session (no authentication)
POST /api/auth/device/        body: {"client_name": "my-script"}
→ {"session_id": "...", "device_code": "...", "user_code": "WXYZ-1234",
   "verification_uri": "https://www.brainstem.org/account/authorize/",
   "verification_uri_complete": "https://www.brainstem.org/account/authorize/?session=...",
   "expires_in": 900, "interval": 5}

# Step 2 — you approve in the browser (with 2FA)

# Step 3 — the tool polls until approved
POST /api/auth/device/token/  body: {"device_code": "..."}
→ {"status": "authorization_pending"}            ← keep polling every 5 s
→ {"status": "success", "token": "...", "token_id": 42, "token_name": "my-script 2026-03-29 14:30:10"}

All poll responses return HTTP 200 with a status in the body:

{"status": "authorization_pending"}   // keep polling
{"status": "success", "token": "..."}  // done; the token is returned exactly once
{"error": "access_denied"}            // you clicked Deny
{"error": "expired_token"}            // the 15-minute window elapsed; start again
{"error": "already_used"}             // the token was already collected

Endpoint structure

All record endpoints follow one pattern:

https://www.brainstem.org/api/{portal}/{app}/{resource}/
https://www.brainstem.org/api/{portal}/{app}/{resource}/{id}/
PartValue
portalprivate (records you can read, needs a token) or public (records of public projects, read-only, no token)
appstem, modules, personal_attributes, resources, taxonomies, dissemination or users
resourcethe lower-case model name, for example session, procedure, specimenset, experimentalinterface or anatomicalregion
idthe UUID of one record

Examples:

https://www.brainstem.org/api/private/stem/session/
https://www.brainstem.org/api/private/stem/session/<id>/
https://www.brainstem.org/api/private/modules/procedure/
https://www.brainstem.org/api/public/stem/project/

taxonomies/brainregion is a deprecated alias of taxonomies/anatomicalregion; use the new name.

Available endpoints

Every resource has its own page with its fields, write rules, permissions and examples. The pages are generated from the server code, so they match the current release.

Core records

  • STEM: projects, subjects, sessions, specimen sets and specimens, cohorts, collections, breedings and epochs.
  • Modules: procedures, procedure episodes and logs, specimen procedures and logs, experimental interfaces, material usages, data acquisition, manipulations, behaviors, equipment, consumable stock, and the templates for each of them.
  • Personal attributes: setups, inventories, behavioral assays, data storage, licenses and protocols.

Shared catalog, taxonomies and dissemination

  • Resources: consumables, hardware devices and suppliers.
  • Taxonomies: species, strains, anatomical regions and atlas references, coordinate transforms, behavioral categories and paradigms, setup types and regulatory authorities.
  • Dissemination: publications and journals.

Collaboration

  • Users: users, groups and laboratories.

Supporting endpoints

  • Templates: the templates that prefill procedures, episodes, interfaces, material rows, data acquisitions and manipulations.
  • Approvals: the approval requests behind submissions to the shared catalogs and taxonomies.
  • Invitations and requests: membership invitations to projects, groups and personal attributes, join requests and pending invitations.
  • Link tables: the many-to-many rows with fields of their own, such as cohort subjects and the interfaces of a data acquisition.

Technical reference

  • Schemas: the type-specific JSON payloads (type + details, coordinate systems) and their versions.
  • Relationships: how resources connect, as the queries that follow each link.
  • Exports: a project or a session as one versioned JSON document.
  • Public schemas: the permanent URLs of every schema version.
  • Agent reference: the server’s own machine-readable description of its API.
  • MCP server: access to the data model and your records for AI applications such as Claude Code and Cursor.

Public vs private portals

Private portal

  • Access: a Personal Access Token, or your browser session in the Web API tool.
  • Content: records of the projects you are a member of and the lab resources shared with your groups.
  • Operations: read, create, change and delete, according to your role on each record.

Public portal

  • Access: open, no token.
  • Content: records of projects that their owners made public, and lab resources made public individually.
  • Operations: read only. An empty public result says nothing about private data.
https://www.brainstem.org/api/public/stem/session/

HTTP methods

  • GET: retrieve a list or one record.
  • POST: create a record.
  • PATCH: change the fields you send (recommended for updates).
  • PUT: update a record with the complete record. Fields you leave out are kept, not cleared, so PATCH does the same with less to send.
  • DELETE: delete a record.

Responses

A list response is an object with the plural response key of the resource and a meta object:

{
    "sessions": [ ... ],
    "meta": {"page": 1, "per_page": 100, "total_results": 231, "total_pages": 3}
}

A detail, create or update response uses the singular key: {"session": {...}}. The keys are listed on each resource page and do not always match the resource name (data_acquisition, consumable_stock, anatomical_region).

Relations are returned as ids. Dates are ISO 8601 strings. Type-specific details are JSON objects validated against the schema of the selected type.

Query parameters

With curl, pass -g (--globoff), because curl otherwise reads {} and [] in a URL as its own globbing syntax, and quote the URL so the shell leaves &, [] and {} alone. The Python and MATLAB tools build these parameters for you.

curl -g -H "Authorization: Bearer $BRAINSTEM_API_TOKEN" \
     "https://www.brainstem.org/api/private/stem/session/?filter{name.icontains}=rat&sort[]=-date_time&per_page=20"

Filtering

/?filter{<field>}=<value>
/?filter{<field>.<operator>}=<value>
/?filter{<field>.<related field>}=<value>      (nestable relations only)

Operators: in, any, all, icontains, contains, startswith, istartswith, endswith, iendswith, iexact, eq, gt, gte, lt, lte, range, isnull, year, month, day, week_day. No operator means exact match. Repeat the parameter for in:

/?filter{type.in}=ExtracellularEphys&filter{type.in}=TwoPhotonMicroscopy

A filter can follow a relation into the related record only when that relation is nestable, that is, when the server lets a query step through it into the related record’s fields (marked “nestable” in the field table of each resource page). Any other relation can only be the last segment and is compared with ids:

QueryWorks?
procedure/?filter{subject}=<subject-id>yes, subject is the last segment
procedure/?filter{target_regions.acronym}=CA1yes, target_regions is nestable
session/?filter{subjects.species}=<species-id>yes, subjects is nestable
procedure/?filter{equipment.name}=Rigno, equipment is not nestable; look the equipment up first and filter with filter{equipment.in}=<ids>

Tags are filtered by name: filter{tags.name}=control (also .in and .icontains); records return tags as names. Type-specific details cannot be filtered on the server.

Sorting

/?sort[]=name
/?sort[]=-date_time
/?sort[]=-date_time&sort[]=name
/?include[]=projects.*
/?include[]=dataacquisition.*&include[]=subjects.*

Included records are returned next to the main list, under their own plural key (“sideloaded”), not nested inside each record. Only nestable relations can be included; on any other relation include[] is ignored and you get ids only. Only records you can read are included, and the relation’s id list in that response then shows only those (see the warning under Writing records). exclude[]=<field> drops a field from the response.

Pagination

Lists are paged with page and per_page:

  • Default page size: 100 records
  • Maximum page size: 500 (larger values are reduced; meta.per_page reports the size used)
  • limit and offset are accepted as aliases when offset is a multiple of limit; other offsets return 400
  • exclude_count=1 skips the total count for faster responses
/?per_page=50&page=3
/?limit=50&offset=100      # the same page

Follow page up to meta.total_pages, or use load_all in the Python and MATLAB tools.

Planned records

Procedures, specimen procedures, sessions and breedings carry a status of planned, performed or cancelled. Listings return performed records only unless you add include_planned=true or filter on status explicitly (filter{status}=planned). A record created without status is recorded as performed. The public portal never returns planned records.

Combining parameters

/?filter{projects}=<project-id>&sort[]=-date_time&include[]=subjects.*&per_page=200

Writing records

  • Create with POST, update with PATCH (send only the fields you change).
  • Required fields are listed on each resource page. The server validates with the same rules as the web forms and answers 400 with {"field": ["message"]}.
  • Lists replace: sending projects, subjects, equipment, target_regions or tags replaces the whole list. Read the record first, change the list, send the full list.
  • JSON payloads replace: details and the coordinate payloads are replaced as a whole. Read, merge, send the full object.
  • Type and payload go together: type picks a schema type and the payload must match it. The resource page names the payload field to send (for example details on procedures, details_json on material usages). The server sets every *_schema_version field itself; do not send one.
  • Some records need form-only inputs that are not returned in responses, for example source on a material usage (inventory, catalog or manual). They are listed under “Form-only inputs” on the resource page.
  • Writes to shared taxonomies and catalog resources by users without approval rights create an approval request instead of the record.
  • Deleting a record that other records still reference returns 400 (“still referenced by …”).

A list read with include[] holds only the linked records you can read. Sending that list back replaces the stored one, so the links to records you cannot see are removed. Read the record without include[] before you change one of its lists.

PUT .../materials/ replaces the whole material collection: every row missing from items is deleted, and a request without items deletes them all. When you send stages, stages missing from them are deleted too. Read the collection with GET first, change it, and send it back with its expected_parent_updated_at; the server answers 409 if the record changed in between.

Other endpoints

EndpointPurpose
GET /api/private/recent-activity/What changed lately in records you can read. Parameters limit (max 200), actor_filter, action_filter, model_type, search, date_from, date_to, ownership, active_projects_only.
GET/POST .../stem/project/<id>/export/, .../stem/session/<id>/export/Project and session exports; see Exports.
GET .../taxonomies/anatomicalregion/<id>/descendants/The regions below a region in its atlas.
GET/PUT .../procedure/<id>/materials/ (also specimen procedures and interfaces)Read or replace the ordered material usage rows of a record.
POST /api/auth/device/, POST /api/auth/device/token/, GET /api/auth/token/info/Device authorization flow and token information.
GET /reference/, GET /schemas/The agent reference and the public schemas; no token needed.
POST /mcp/The MCP server.

Error handling

StatusMeaningWhat to do
200 / 201 / 204success / created / deleted 
400 {"field": [...]}validation failedfix the named fields
400 ["Invalid nested filter field: x"]a filter followed a relation that is not nestablefilter by ids instead
401token missing, invalid, expired or revokedsign in again or create a new token
403no permission for this record or actionask a project manager for the role you need
404no such record, or not visible to youthe record may exist in a project you cannot read
409stale write (material lists) or export job not readyre-read and retry, or wait
429rate limitedwait and retry; do not loop

Rate limits

ScopeLimit
Anonymous requests (public portal)60 per minute per IP
Agent reference (/reference/) and public schemas (/schemas/)600 per minute per IP, each
Authenticated requests300 per minute per user
Device flow10 session starts and 30 polls per minute
Exports (direct and background jobs together)30 per hour per user
MCP messages300 per minute

Requests over the limit receive 429 with a Retry-After header.