The public REST API lets scripts, CI jobs and the Orbit CLI use Orbit with a
workspace API token. The base path is <public origin>/api/public/v1.
The OpenAPI document in the repository (packages/api/src/generated/openapi.json,
or orbit openapi --output <file>) describes this API under the tag public-api.
This API is separate from the internal /api/v1 API of the web and mobile apps.
The internal API can change in each release. Use only the public API in your
scripts.
- As a workspace owner or administrator, open Settings → API tokens. Other members (not guests) can create only a personal export token there (see Exports).
- Select the projects, the permissions and an expiry. Then create the token.
project_idsis required, with at least one project, whenscopesincludestasks:readortasks:write. Alternatively, setall_projects: true(the All projects, including future ones box): the token then covers every live project of the workspace, also projects created later. Without a task scope, projects have no effect: the token stores none (project_ids: []). - Copy the token (
orb_…). Orbit shows it one time only. - Send the token in the
Authorizationheader of each request:
export ORBIT_URL='https://orbit.example.com'
read -rs -p 'Orbit API token: ' ORBIT_TOKEN; echo
export ORBIT_TOKEN
curl -sS "$ORBIT_URL/api/public/v1/me" -H "Authorization: Bearer $ORBIT_TOKEN"Do not put the token in a URL, in a repository, or in a shared shell history.
Each token belongs to one workspace and to the projects that you selected, or to
all live projects of the workspace when you selected all projects.
Task and doc paths contain the workspace ID:
/api/public/v1/workspaces/{workspace_id}/…. Get the ID from GET /me. A
path with another workspace ID returns 404. The API does not use cookies, sessions or CSRF tokens. Requests
without an Origin header are accepted. If a request has an Origin header, the
origin must be the Orbit public origin. The X-Orbit-Contract header is not
necessary.
A personal token acts as the person who created it. Orbit checks on each request that this person is still a member of the workspace and that their account is not suspended. If not, the token stops working.
A service-account token does not depend on the person who created it. It keeps
working when that person leaves the workspace, changes role or is suspended,
until an owner or administrator revokes it. When you revoke a token and the
service account has no other token that is not revoked and not expired, Orbit
disables the service account; its past work keeps its
name. A new token with the same service-account name enables it again. The
GitHub integration's service account (named GitHub) is never disabled this way.
A service-account token can use tasks only, not docs. Orbit refuses to create a service-account token
with docs:read, docs:write or chat:read (422). Its writes show the service account as the author:
as the creator of the tasks that it makes, and as the author of its comments.
Its writes do not send notifications (assignments and mentions), because no
member made them.
The audit log records each task and comment write with the ID of the token
(api_token_id). A personal token shows its creator as the actor. A
service-account token shows the service account as the actor.
API tokens and MCP OAuth connections use the same five resource scopes. In Settings → API tokens, set the access for each resource to No access, Read or Write. API tokens also have an import scope and two export scopes, which MCP does not use.
| Scope | Access in Settings → API tokens | Public API endpoints |
|---|---|---|
tasks:read |
Tasks: Read | GET projects, statuses, labels, members, tasks and comments |
tasks:write |
Tasks: Write | Create, update and delete tasks, add comments, link tasks, create and change projects, statuses and labels, POST /integrations/discord/events |
tasks:import |
Tasks: Write with Import mode | With tasks:write: the import fields of new tasks and comments. See Import from another tool. |
docs:read |
Docs: Read | GET /docs, GET /docs/{page_id}, GET /docs/{page_id}/database, POST /docs/{page_id}/database/query |
docs:write |
Docs: Write | POST /docs, PATCH /docs/{page_id}, POST /docs/{page_id}/rows, PATCH /docs/{page_id}/rows/{row_id}, POST /docs/{page_id}/rows/trash |
chat:read |
Chat: Read | None. Chat is available through MCP only. A bot that reads and writes one channel uses a chat webhook with events. |
workspace:export |
Workspace export | POST /exports with mode: "full", GET /exports/{export_id}, GET /exports/{export_id}/download |
workspace:export:personal |
Personal export | The same, with mode: "personal" |
tasks:write includes tasks:read, and docs:write includes docs:read.
GET /me works with each valid token.
An export scope must be the only scope of a personal token: Orbit refuses a
service-account token with an export scope, or an export scope together with
another scope (422 invalid_api_token). workspace:export needs a workspace
owner or administrator, when you create the token and on each request.
workspace:export:personal is open to each member except guests.
tasks:import needs tasks:write (422 invalid_api_token otherwise). Its
creator must be a workspace owner or administrator on each write: when the
creator loses that role, each write of the token gets 403 workspace_action_forbidden.
- Field names are the same as in the internal Orbit records, so clients can share models.
- IDs are UUID strings. Times are RFC 3339 strings in UTC with milliseconds, for
example
2026-11-04T09:30:00.000Z. - A
{task}path segment is a task UUID or a task identifier such asENG-12(the letter case is not important). - A
{task}value that is neither a UUID nor an identifier gives400(invalid_request) on every method. An identifier of no visible task gives404. - A task's or doc's
versionis an opaque number below 2^53. It changes with every change, including live edits in the editor. Send it back unchanged asexpected_version, and compare it only for equality; it does not count up. - Lists return
{"items": [...], "next_cursor": string | null}. To get the next page, sendnext_cursorascursorwith the same other parameters.next_cursorisnullon the last page and on lists that have no pages. limitis 1 to 100. The default is 50.- A task contains no embedded parent, duplicate or ancestor records.
parent_task_idisnullwhen the parent is not in an approved project.
GET /me. Scope: none.
curl -sS "$ORBIT_URL/api/public/v1/me" -H "Authorization: Bearer $ORBIT_TOKEN"{
"token": {"id": "…", "name": "ci", "scopes": ["tasks:read", "tasks:write"],
"project_ids": ["…"], "expires_at": "2026-12-01T00:00:00.000Z"},
"workspace": {"id": "…", "name": "Acme"},
"workspaces": [{"id": "…", "name": "Acme", "scopes": ["tasks:read", "tasks:write"],
"project_ids": ["…"]}],
"user": {"id": "…", "email": "dev@example.com", "display_name": "Dev"},
"service_account": null
}For a service-account token, user is null and service_account is
{"id", "name"}.
workspaces lists each workspace that the token can use, with the scopes and
projects there. Today a token has exactly one workspace, so the list has one
item, the same as workspace. Read workspaces in new clients: a later version
can give a token more than one workspace.
In the examples below, set the workspace first:
export ORBIT_WS="$ORBIT_URL/api/public/v1/workspaces/$WORKSPACE_ID"GET /workspaces/{workspace_id}/projects. Scope: tasks:read. The approved live projects. Item:
{id, name, key, color, version, created_at, updated_at}.
curl -sS "$ORBIT_WS/projects" -H "Authorization: Bearer $ORBIT_TOKEN"POST /workspaces/{workspace_id}/projects. Scope: tasks:write, and the token must cover
all projects; a token for chosen projects gets 403 all_projects_required, because it could
not use the new project. Body: {name, key, color?}. key is the prefix of task identifiers:
1 to 20 letters, digits or underscores, unique in the workspace, stored in upper case. color is
#rrggbb (default #6366f1). The project gets the default workflow statuses. Answers 201 with
the project, or 400 when the key is already used.
curl -sS -X POST "$ORBIT_WS/projects" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' --data '{"name":"Video series","key":"VID"}'PATCH /workspaces/{workspace_id}/projects/{project_id}. Scope: tasks:write. Body:
{expected_version, name?, color?}. A missing field keeps its value. Answers 200 with the
project, or 409 when the project changed after you read it. The key does not change.
GET /workspaces/{workspace_id}/projects/{project_id}/statuses?cursor&limit. Scope: tasks:read. Item:
{id, project_id, name, description, color, category, position, version}.
category is unstarted, started, completed, cancelled or duplicate.
curl -sS "$ORBIT_WS/projects/$PROJECT_ID/statuses" \
-H "Authorization: Bearer $ORBIT_TOKEN"POST /workspaces/{workspace_id}/projects/{project_id}/statuses. Scope: tasks:write. Body:
{name, category, color?, description?, position?}. category is unstarted, started,
completed or cancelled: tasks in a completed or cancelled status are done. Orbit makes
the duplicate status itself. Without color, the status gets the color of its category.
Without position, it goes after the last status. Answers 201 with the status.
PATCH /workspaces/{workspace_id}/projects/{project_id}/statuses/{status_id}. Scope:
tasks:write. Body: {expected_version, name?, category?, color?, description?, position?}. A
missing field keeps its value. The Duplicate status cannot change its category, and no other
status can become duplicate (400). Answers 200 with the status, or 409 after a change.
curl -sS -X POST "$ORBIT_WS/projects/$PROJECT_ID/statuses" \
-H "Authorization: Bearer $ORBIT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"In review","category":"started"}'GET /workspaces/{workspace_id}/projects/{project_id}/labels?cursor&limit.
Scope: tasks:read. Item: {id, project_id, name, color, version}. Use the
IDs in label_ids.
curl -sS "$ORBIT_WS/projects/$PROJECT_ID/labels" \
-H "Authorization: Bearer $ORBIT_TOKEN"POST /workspaces/{workspace_id}/projects/{project_id}/labels. Scope: tasks:write. Body:
{name, color?}: 1 to 100 characters, and #rrggbb (gray when unset). Answers 201 with the
label, or 400 when the project already has a label with this name.
PATCH /workspaces/{workspace_id}/projects/{project_id}/labels/{label_id}. Scope:
tasks:write. Body: {expected_version, name?, color?}. A missing field keeps its value.
Answers 200 with the label, or 409 after a change.
GET /workspaces/{workspace_id}/members?cursor&limit. Scope: tasks:read. The
workspace members, to find assignee IDs. Item: {user_id, display_name}. The
API does not show email addresses or roles.
curl -sS "$ORBIT_WS/members" -H "Authorization: Bearer $ORBIT_TOKEN"A task:
{
"id": "…", "project_id": "…", "number": 12, "status_id": "…",
"title": "Fix login", "description": "Markdown text", "priority": "high",
"assignee_ids": ["…"], "label_ids": ["…"],
"due_start_at": null, "due_at": null,
"created_at": "…", "updated_at": "…", "completed_at": null,
"creator_id": "…", "creator_service_account": null, "external_id": null,
"version": 4503599627370495,
"parent_task_id": null,
"identifier": "ENG-12",
"url": "https://orbit.example.com/tasks/…?workspace=…"
}identifier is the project key and the task number. Use it in {task} paths.
url is the task's page in the Orbit web app.
completed_at is when the task entered a completed or cancelled status, and null while it
is open. creator_id is the member who created the task. For a task that a service account
created, creator_id is null and creator_service_account is {id, name}. external_id is the
source tool's ID of an imported task (see Import from another tool),
and null otherwise.
priority is none, low, medium, high or urgent.
GET /workspaces/{workspace_id}/tasks. Scope: tasks:read. Tasks in the approved projects, newest first,
including completed tasks and sub-issues. Optional parameters: project_id,
status_id, assignee_id (a user ID, or none for tasks without an
assignee), label_id, priority, parent_task_id (only the direct sub-issues
of this task, as a UUID or an identifier such as ENG-12; the task must be in an approved project), q (text in the title or description, or an
identifier), sort (created_at, updated_at, priority, due_date or
title; default created_at), order (desc or asc; default desc; with
sort=priority, desc gives urgent first and none last),
cursor and limit. Keep the same parameters when you send cursor.
curl -sS -G "$ORBIT_WS/tasks" -H "Authorization: Bearer $ORBIT_TOKEN" \
--data-urlencode "project_id=$PROJECT_ID" --data-urlencode 'q=login' -d limit=20GET /workspaces/{workspace_id}/tasks/{task}. Scope: tasks:read.
curl -sS "$ORBIT_WS/tasks/ENG-12" -H "Authorization: Bearer $ORBIT_TOKEN"POST /workspaces/{workspace_id}/tasks. Scope: tasks:write. Response: 201 with the task. Body:
project_id and status_id (required), title (required, 1 to 500
characters), and the optional description (Markdown, up to 50,000 bytes),
priority (default none), assignee_ids, label_ids, parent_task_id,
due_start_at and due_at (RFC 3339 times; due_start_at needs due_at and
must not be after it).
The assignees must be members of the workspace. The labels and the status must
be in the task's project. The parent and all the tasks related to it must be in
approved projects. parent_task_id takes a task UUID or an identifier such as ENG-12.
curl -sS "$ORBIT_WS/tasks" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"project_id\":\"$PROJECT_ID\",\"status_id\":\"$STATUS_ID\",\"title\":\"Fix login\",\"priority\":\"high\"}"PATCH /workspaces/{workspace_id}/tasks/{task}. Scope: tasks:write. Response: 200 with the task. Body:
expected_version (required, the version that you read), and the optional
title, description, status_id, priority, assignee_ids, label_ids,
due_start_at, due_at, parent_task_id and duplicate_of_id. A list replaces the old list.
duplicate_of_id (a task UUID or identifier in an approved project) marks the task as a
duplicate of that task and moves it to the Duplicate status; null unmarks it. A missing field keeps
its value, null clears a due date, and parent_task_id: null detaches the task from its
parent (a new parent is a task UUID or an identifier such as ENG-12). A new parent and all the tasks related to it must be in approved projects. If the task changed after you read it, the
response is 409. This includes edits that people make to the description in
the editor. Read the task again, then try again. All the tasks in the
task's parent and sub-issue tree must be in approved projects.
curl -sS -X PATCH "$ORBIT_WS/tasks/ENG-12" \
-H "Authorization: Bearer $ORBIT_TOKEN" -H 'Content-Type: application/json' \
-d '{"expected_version":4503599627370495,"status_id":"…","assignee_ids":[]}'DELETE /workspaces/{workspace_id}/tasks/{task}?expected_version={version}. Scope: tasks:write. Moves the task and
its sub-issues to the trash. Response: 204 with no body. The tasks stay in the Orbit
trash for 30 days, where members can restore them in Orbit. Then Orbit deletes them
forever. After the delete, GET answers 404 for these tasks.
curl -sS -X DELETE "$ORBIT_WS/tasks/ENG-12?expected_version=4503599627370495" \
-H "Authorization: Bearer $ORBIT_TOKEN"Add permanent=true to delete the task forever. You cannot undo this. It removes
the task and its sub-issues with their comments, attachments (and the stored
files), relations and description, as the end of the 30 days in the trash does.
Stored files are deleted at the next daily maintenance run. Response: 204.
- A task that is not in the trash needs its current
expected_version(409otherwise). Its sub-issues that are not in the trash go with it. - It also works for a task that is already in the trash, for example one that
you moved there earlier. The sub-issues that went to the trash with it go
too. A task in the trash cannot change, and
GETcannot read it, so Orbit does not checkexpected_version(send the last version that you read, or any number). - Like deleting teamspace docs forever, it needs a token of a workspace owner or
admin, or a service account token. Other tokens get
403(workspace_action_forbidden), and the task does not change.
curl -sS -X DELETE "$ORBIT_WS/tasks/ENG-12?expected_version=4503599627370495&permanent=true" \
-H "Authorization: Bearer $ORBIT_TOKEN"GET /workspaces/{workspace_id}/tasks/{task}/relations. Scope: tasks:read. Every relation of
the task, as seen from it: {id, type, direction, task, created_at}. type is blocks,
related or duplicate. direction is outgoing when this task is the blocker or the
duplicate. task is the other task: {id, project_id, number, title, status_id, version}.
Relations to tasks outside the approved projects are left out. next_cursor is null.
POST /workspaces/{workspace_id}/tasks/{task}/relations. Scope: tasks:write. Body:
{type, task_id}. type is blocks (this task blocks task_id), blocked_by or related.
task_id is a task UUID or identifier in an approved project (422 otherwise). A relation that
already links the two tasks is replaced, and the same relation again is returned unchanged. A
duplicate relation is not replaced (409). Answers 201 with the relation.
DELETE /workspaces/{workspace_id}/tasks/{task}/relations/{relation_id}. Scope: tasks:write.
Answers 204. Removing a duplicate relation also moves the duplicate out of the Duplicate status.
Make duplicates with duplicate_of_id on PATCH /tasks/{task}.
curl -sS -X POST "$ORBIT_WS/tasks/ENG-12/relations" \
-H "Authorization: Bearer $ORBIT_TOKEN" -H 'Content-Type: application/json' \
-d '{"type":"blocks","task_id":"ENG-14"}'GET /workspaces/{workspace_id}/tasks/{task}/comments?cursor&limit. Scope:
tasks:read. Oldest first. Item:
{id, task_id, author_id, author_service_account, body, parent_id, external_id, version, created_at, updated_at}. author_service_account is {id, name} for a comment
that a service account wrote, and null otherwise. Show it as the author when
it is set.
body is Markdown.
curl -sS "$ORBIT_WS/tasks/ENG-12/comments" -H "Authorization: Bearer $ORBIT_TOKEN"POST /workspaces/{workspace_id}/tasks/{task}/comments. Scope: tasks:write.
Body: {"body": "Markdown", "parent_id"?: "…"} (1 to 100,000 bytes in UTF-8, not only
spaces). parent_id is a comment of the same task: the new comment is its reply. Response:
201 with the comment. The author is the token's creator, or for a service-account token
the service account.
curl -sS "$ORBIT_WS/tasks/ENG-12/comments" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' -d '{"body":"Deployed in build 512."}'A token with tasks:write and tasks:import (Import mode in Settings → API tokens)
copies tasks from another tool, such as Plane, Jira or a tool of your own, as the built-in
importers do:
- Its writes notify nobody (assignees, mentions) and run no sub-issue automation, also its updates. The source tool already decided the state of each task.
- New tasks and comments take the import fields below. A token without
tasks:importthat sends one of them gets403 insufficient_scope.
| Field | On | Effect |
|---|---|---|
external_id |
tasks, comments | The source tool's ID, 1 to 200 characters, unique in the workspace. A second create with the same ID answers 200 with the item that the first create made, and changes nothing. A restarted import thus makes no duplicates. An ID whose task is in the trash, or a comment ID on another task, answers 409. |
number |
tasks | The task number, so ABC-166 stays ABC-166. It must be free in the project, also among tasks in the trash (409). Later tasks get numbers after the highest one. |
created_at, updated_at |
tasks, comments | The original times (RFC 3339). updated_at needs created_at and must not be before it; it is created_at when unset. |
completed_at |
tasks | When the task was done. Kept when the status is completed or cancelled. |
creator_id |
tasks | The member shown as the creator. |
author_id |
comments | The member shown as the author. |
author_name |
comments | An author who is not a member, shown in bold on the first line of the comment. Not with author_id. |
creator_id and author_id must be active members who are not guests (422). The audit
log still names the token.
A typical import:
- Create the projects (
POST /projects, with a token for all projects), then their statuses and labels. - Create the tasks, parents before their sub-issues, with
external_id,numberand the original times. - Add the comments, oldest first, with
external_id, the author and the original times. - Link the tasks with
POST /tasks/{task}/relations, and mark duplicates withduplicate_of_id.
If the script stops, run it again: each create with an external_id that Orbit already has
returns the existing item.
curl -sS "$ORBIT_WS/tasks" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"project_id\":\"$PROJECT_ID\",\"status_id\":\"$STATUS_ID\",\"title\":\"Fix login\",
\"external_id\":\"plane:6f1c…\",\"number\":166,\"created_at\":\"2024-03-01T09:30:00.000Z\"}"Docs show only the documents that the token's creator can see, including their
private docs. The docs:read and docs:write scopes are off by default. Doc
writes act as the token's creator: their name shows as the last editor, and
mentions in the Markdown notify the mentioned members.
GET /workspaces/{workspace_id}/docs. Scope: docs:read. Item: {id, parent_id, teamspace_id, private, title, icon, position, version, updated_at}. next_cursor is always null.
curl -sS "$ORBIT_WS/docs" -H "Authorization: Bearer $ORBIT_TOKEN"GET /workspaces/{workspace_id}/docs/{page_id}. Scope: docs:read. The document record, as the MCP
get_doc tool returns it. content is the editor's BlockNote JSON, and
markdown is the same content as GitHub-flavored Markdown, the format that doc
writes take. POST and PATCH return the same record. Links to other docs show
as [Title](/docs/{page_id}). A doc that the token's creator cannot see shows as
[Untitled](/docs/{page_id}).
curl -sS "$ORBIT_WS/docs/$PAGE_ID" -H "Authorization: Bearer $ORBIT_TOKEN"POST /workspaces/{workspace_id}/docs. Scope: docs:write. Body: title
(optional, up to 500 characters), markdown (optional, up to 200,000 bytes),
icon (optional, up to 64 characters) and the place of the doc: parent_id
(the doc goes under this doc, in its space), or for a root doc teamspace_id or
"private": true. Without a place, the doc goes to the root of the default
teamspace. The doc and its content are created in one step. Response: 201
with the document record. A doc can have at most 4,000 blocks (paragraphs,
headings, list items and other blocks, nested ones included). Markdown with
more blocks gets 413 (doc_too_large), and no doc is created.
curl -sS "$ORBIT_WS/docs" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"Release notes","markdown":"## Build 512\n\n- Faster sync"}'PATCH /workspaces/{workspace_id}/docs/{page_id}. Scope: docs:write. Body:
expected_version (required) and optional title, icon (null clears it)
and markdown. markdown is the doc's whole new content. Send the markdown
you read, with your edits. Orbit compares it with the current content and
changes only what you changed, also for people who have the doc open. Blocks you
leave as read stay exactly as they are, with their colors, comments and other
settings. An edited block keeps its settings and its kind: a toggle stays a
toggle, a callout stays a callout. Text you did not touch keeps its colors and
comments. Markdown cannot create colors, alignment, toggles, callouts, comments,
file or sub-page blocks, image sizes or table cell styling: new blocks get only
what Markdown can show. A write that changes nothing (the same title, icon and
content) changes no data: the response has the same version, so other clients
that hold it can still write. Orbit checks expected_version first, so a stale
version gets 409 also for such a write. If the doc changed after you read it (title, icon or content,
including live edits in the editor), the response is 409. Read the doc again,
then try again. The version in the doc list is the same value. Response: 200
with the document record. A locked doc answers 423 (page_locked).
markdown that gives the doc more than 4,000 blocks, or any markdown for a
doc that already has more than 4,000 blocks (for example, from the editor), gets
413 (doc_too_large), and nothing changes. Such a doc keeps its title and
icon writes; edit its content in Orbit, or split it into several docs.
curl -sS -X PATCH "$ORBIT_WS/docs/$PAGE_ID" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' -d '{"expected_version":4503599627370495,"markdown":"Updated."}'A database is a doc with "kind": "database" in the doc list. Its rows are docs
too: GET /docs/{row_id} returns a row's body, its database_id and its
values. The database endpoints use the docs scopes and show only the
databases that the token's creator can see. Writes act as the token's creator.
Values use the same keys and names that people see:
- Keys are property names or property IDs. A name matches in any letter case
when no property has the exact name. A name that two properties share gets
422: use the ID. - Select and status take an option name or ID. Multi-select takes a list
of them. A value never adds an option: an unknown option gets
422. - Person takes a list of user IDs or member emails, or
"me". - Date takes
YYYY-MM-DD, a UTC time (2026-11-09T14:00:00Z), the intervalstart/end, or{"start": …, "end": …}. A time can also have a UTC offset (2026-11-10T14:00:00+01:00): Orbit stores it as the UTC time (2026-11-10T13:00:00Z). A date without a time stays a date. Date filters take the same forms. - Relation takes a list of page IDs of rows of the related database. Titles are not accepted.
- Text, URL and number take their value; a percent number stores
0.5for 50%. Checkbox takestrueorfalse.nullclears a value. - Results show option names, people as
{id, name}, dates as{start, end}, related pages as{id, title}and unique IDs as"PREFIX-12"("12"without a prefix), and every property except the title and the created and edited ones (the row has these as its own fields). Two properties with the same name show under their IDs. Rollups and formulas show as computed when the database is read; a formula's people and pages show as{id, name}and{id, title}too. A relation shows only the pages that the token's creator can read; the row'shidden_linkscounts the others per relation key (absent when there are none). - Unique ID filters take
"PREFIX-12"(the prefix in any letter case),"12"or12.
GET /workspaces/{workspace_id}/docs/{page_id}/database. Scope: docs:read.
The database's title, url, locked, row_count, properties (id, name,
type, options with names, and the rest of the config), views (id,
name, type) and templates (id, name).
POST /workspaces/{workspace_id}/docs/{page_id}/database/query. Scope:
docs:read. Body (all optional):
filter: an AND/OR group, as a view's filter: up to 3 levels, 10 children per group and 20 rules. A rule is{property_id, operator, value}, with a property name or ID. A rule without a value matches every row.sorts: up to 10{property_id, direction},directionasc(default) ordesc. Empty values sort last.view_id: starts from this view's filter and sorts.filtermust then match too, andsortsreplace the view's.limit(1 to 100, default 50),cursor(thenext_cursorof the previous page), andtz(an IANA zone such asEurope/Budapestfor relative dates and computed values; defaultUTC).
Response: {items, next_cursor, total}. Each item is a row: id, title,
icon, url, created_at, created_by, updated_at, updated_by and
values. Orbit filters and sorts on the server with the same rules as the web
app. A cursor works only with the same filter, sorts, view and zone; another
query with it gets 422 (cursor). A 422 names the invalid field in
errors, with a message that says what the field needs, such as limit:
"Send a limit from 1 to 100."
curl -sS "$ORBIT_WS/docs/$DATABASE_ID/database/query" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"filter":{"op":"and","children":[{"property_id":"Status","operator":"is","value":["In progress"]},{"property_id":"Due","operator":"on_or_before","value":"today"}]},"sorts":[{"property_id":"Due","direction":"asc"}],"tz":"Europe/Budapest"}'POST /workspaces/{workspace_id}/docs/{page_id}/rows. Scope: docs:write.
Body (all optional): title (up to 500 characters), values (up to 50),
markdown (the row's body) and template_id (a template of the database, which
fills the title, values and body; title and values in the body win, and
markdown replaces the template's body). The row goes last. Response: 201
with the row. A database holds at most 5,000 rows (409
database_row_limit).
PATCH /workspaces/{workspace_id}/docs/{page_id}/rows/{row_id}. Scope:
docs:write. Body: values (up to 50), title, or both. The last write wins,
as in the web app: there is no expected_version. A locked database still takes
value writes; the lock covers its properties and views. Response: 200 with the
row.
POST /workspaces/{workspace_id}/docs/{page_id}/rows/trash. Scope:
docs:write. Body: row_ids (1 to 100). Each row goes to the trash for 30
days with its sub-pages. IDs that are not live rows of the database are
skipped. Response: {"trashed": n}.
curl -sS -X PATCH "$ORBIT_WS/docs/$DATABASE_ID/rows/$ROW_ID" -H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"values":{"Status":"Done","Owner":["ada@example.com"],"Due":"2026-11-20"}}'The schema endpoints add, change and delete a database's properties (its
columns). They need docs:write and act as the token's creator, who must be
able to edit the database. A locked database refuses them (423
page_locked). {property} is a property name or ID, with the same matching
as value keys.
POST /workspaces/{workspace_id}/docs/{page_id}/database/properties. Body:
name (1 to 200 characters), type (any type but title: text, number,
select, multi_select, status, date, person, checkbox, url,
email, phone, files, unique_id, relation, rollup, created_time,
created_by, last_edited_time, last_edited_by, button or formula), and
optional config and position (index among the properties; omitted appends
it). Select, multi-select and status options in config can be names or
{name, color, group} objects. Colors: default, gray, brown, orange,
yellow, green, blue, purple, pink, red. group (todo,
in_progress or done) is only for a status. Without config, a type gets its
default (a status gets Not started, In progress and Done). Every view shows the
new property. Response: 201 with the property (id, name, type, config,
position). A database holds at most 100 properties (409
database_property_limit).
PATCH /workspaces/{workspace_id}/docs/{page_id}/database/properties/{property}.
Body: one or more of name, type, config, add_options and position,
and optional dry_run.
typeconverts the values (it clears the values that cannot convert) and gives the type's default config. To set the config too, send it in a later request.configreplaces the whole config. Options left out are removed from every row. An option sent by name only keeps the ID, color and group of the option with that name, so its values stay.add_optionsadds options after the current ones and skips names the property already has (in any letter case). Send it withoutconfig.dry_run: truereturns the result without saving it.
Response: 200 with {property, converted, cleared}: the counts of values
that a type change converted, and of values that a type change or a removed
option cleared. The title property cannot change its type (409
database_title_property).
DELETE /workspaces/{workspace_id}/docs/{page_id}/database/properties/{property}.
Deletes the property and its values in every row. You cannot undo this. The
title property cannot be deleted (409 database_title_property). Response:
204.
curl -sS -X PATCH "$ORBIT_WS/docs/$DATABASE_ID/database/properties/Status" \
-H "Authorization: Bearer $ORBIT_TOKEN" -H 'Content-Type: application/json' \
-d '{"add_options":["Blocked",{"name":"Shipped","color":"green"}]}'An export is a ZIP archive that another Orbit instance can import. The export
paths have no workspace ID: an export always belongs to the token's workspace
and to the token's creator. The archive's README.txt and manifest.json
describe its layout.
| Mode | Scope | The archive holds |
|---|---|---|
full |
workspace:export |
The workspace as the token's creator sees it: tasks, projects, teamspaces, docs (also their own private docs), public channels, and the private channels and DMs that they are in. |
personal |
workspace:export:personal |
Only the creator's private docs (with their files and comments), the private channels and DMs that they are in, and the members who appear in them. |
POST /exports starts an export. The mode must match the token's scope
(else 403 insufficient_scope). If the token's creator already has an export
of this mode that is queued or running, Orbit returns it and does not start
another one. Thus you can send the request again after an error. A ready
export is not reused: each POST after it finished starts a fresh one.
curl -sS -X POST "$ORBIT_URL/api/public/v1/exports" \
-H "Authorization: Bearer $ORBIT_TOKEN" -H 'Content-Type: application/json' \
-d '{"mode": "full"}'{"id": "…", "status": "queued", "mode": "full", "requested_by_name": "Dev",
"size_bytes": null, "created_at": "…", "completed_at": null, "expires_at": "…"}The status is 202 while the export is queued or running, and 200 when
it is ready or failed. GET /exports/{export_id} returns the same record;
poll it until the status is ready or failed. A token sees only the exports
of its creator in its own mode (404 for others). An export expires seven days
after it is ready.
GET /exports/{export_id}/download streams the ZIP
(Content-Type: application/zip). It returns 409 export_not_ready until the
export is ready.
POST /integrations/discord/events. Scope: tasks:write. A Discord bot sends a
message here to make a task from it. Body: event_id (required, 1 to 200
characters, the same ID for a retry), message (required), message_url
(required, a https://discord.com/channels/… link) and project_id (optional
when the token has one project only). Response: 201 with
{"task": {...}, "duplicate": false}, or 200 with "duplicate": true when
Orbit already has a task for this event_id. This endpoint was at
/api/v1/integrations/discord/events before. Update the bot configuration to
the new path.
This endpoint has its own error codes, not the codes in Errors:
| Status | code |
Cause |
|---|---|---|
| 400 | invalid_request |
The body is not valid JSON or has unknown fields. |
| 401 | api_token_required |
No Authorization: Bearer token. |
| 401 | invalid_api_token |
The token is not correct, expired or revoked. |
| 402 | workspace_read_only, trial_limit_reached |
Orbit Cloud: the workspace has no active subscription, or the trial limit is reached. |
| 403 | api_token_scope_forbidden |
The token does not have tasks:write. |
| 409 | integration_event_conflict |
The event_id was used before with a different message or message_url. |
| 422 | validation_failed |
A field is not valid, or project_id is not a project of the token. errors names the field. |
| 422 | integration_project_unavailable |
The project or its first status is not available. |
| 500 | internal_error |
Orbit could not complete the request. Try again later. |
curl -sS "$ORBIT_URL/api/public/v1/integrations/discord/events" \
-H "Authorization: Bearer $ORBIT_TOKEN" -H 'Content-Type: application/json' \
-d '{"event_id":"1234","message":"Login fails","message_url":"https://discord.com/channels/1/2/3"}'Errors are RFC 9457 problem documents (Content-Type: application/problem+json):
{
"type": "https://docs.orbit.dev/problems/not_found",
"title": "Not found", "status": 404, "code": "not_found",
"detail": "The resource does not exist or this token cannot use it.",
"instance": "/api/public/v1/workspaces/…/tasks/PRV-3",
"request_id": "…"
}| Status | code |
Cause |
|---|---|---|
| 400 | invalid_request, invalid_cursor |
A parameter or the body is not correct. Orbit checks the token and its scope first, so a token without the scope gets 403, also with an incorrect body. |
| 401 | unauthorized |
No token, or the token is not correct, expired or revoked. The response has WWW-Authenticate: Bearer realm="orbit". |
| 402 | workspace_read_only, trial_limit_reached |
Orbit Cloud: the workspace has no active subscription, or the trial limit is reached. |
| 403 | insufficient_scope |
The token does not have the scope of the endpoint. |
| 403 | workspace_action_forbidden |
The token's member may not do this, for example delete a task forever (permanent=true) or run a full export without being a workspace owner or admin. |
| 404 | not_found |
The resource does not exist, is in the trash, or is not in an approved project. |
| 405 | method_not_allowed |
The path does not take this HTTP method, for example PUT /tasks. The Allow header lists the methods that it takes (GET,HEAD,POST). Orbit sends this before it checks the token. |
| 409 | conflict |
The resource changed after you read it (for docs and task descriptions, also through live edits). conflict.current_version has the current version. |
| 409 | task_conflict, github_content_read_only, github_linked_move |
The change conflicts with the task's current state. |
| 409 | export_not_ready |
The export is queued, running or failed, so there is no archive to download yet. |
| 413 | doc_too_large |
The doc would have, or already has, more than 4,000 blocks. Nothing changed. |
| 422 | validation_failed, parent_cycle, parent_invalid |
A value is not valid for the workspace or project, for example an unknown assignee or label. errors names the field. |
| 423 | page_locked |
The doc is locked. Unlock it in Orbit to change its title, icon or content. |
| 429 | rate_limit_exceeded |
Too many requests. Wait as the Retry-After header tells (RFC 9110). Orbit sends a number of seconds. |
| 500 | internal_error |
Orbit could not complete the request, for example because the database is unavailable. Try again later. |
| 503 | task_busy, page_busy, service_unavailable |
The task's description or the doc's content is busy on the server, or a service is unavailable. Try again later. |
Orbit returns 404, not 403, for a resource in a project that the token
cannot use. Thus a token cannot find out which other resources exist.
- Rate limit: the public API has its own budget, apart from the web app's, so
busy tokens do not slow down the members who use Orbit in the browser. Every
/api/public/endpoint and MCP (/mcp) share it. Its limit is the general limit (ORBIT_RATE_LIMIT_GENERAL_PER_MINUTE) for each client IP address and, separately, for each token. All the tokens of one workspace also share a workspace budget (ORBIT_RATE_LIMIT_WORKSPACE_MULTIPLIERtimes the general limit) on the/workspaces/{workspace_id}/…paths. Only successful responses (status below 400) count against the workspace budget, so requests with an unknown token or a wrong method cannot use it up. Each server counts on its own: with N servers, the limits are N times the setting. - The usual request body limit and request timeout of the Orbit server apply.
- Databases: 100 rows per query page, 100 rows per trash request, and 50 values per row write.
- The token's expiry, its revocation and the projects in its grant apply immediately. A deleted project leaves the grant.
cargo test -p orbit-server --test public_api
ORBIT_TEST_POSTGRES_URL=postgres://orbit:orbit@127.0.0.1:15432/orbit \
cargo test -p orbit-server --test public_api