Skip to content

REST API ​

EOS Hub has a REST API for integrations -- automation tools such as n8n or Zapier, reporting scripts, spreadsheets, or your own apps. It returns the same data you see in the web app, as JSON.

Current scope

You can read organizations, teams, to-dos, issues, Rocks, the Scorecard, meetings, the V/TO, the Accountability Chart, People Analyzer entries, and documents.

You can create, change, and delete to-dos, issues, Rocks with their milestones, and Scorecard measurables with their weekly values -- see Writing Data. Meetings, the V/TO, the Accountability Chart, People Analyzer entries, documents, and organization and platform administration are read only through the API for now; writing them is planned for a later release.

Base URLhttps://<your-host>/api/v1
Interactive referencehttps://<your-host>/api/v1/docs -- browse every endpoint and try calls with your own key
OpenAPI spechttps://<your-host>/api/v1/openapi.json (OpenAPI 3.1) -- for code generators and API clients
AuthenticationAuthorization: Bearer ak_… (personal API key)
FormatJSON, camelCase field names

<your-host> is the address of your EOS Hub installation -- the same one you open in the browser.

Authentication and API Keys ​

Every request needs a personal API key in the Authorization header:

http
Authorization: Bearer ak_k3m7q2xa_Vq0…

Create keys in the web app under Settings > API keys -- see API Keys for the full walkthrough. In short:

  • The full key is shown only once, right after you create it. EOS Hub stores only a hash of it, so a lost key cannot be recovered -- revoke it and create a new one.
  • Only API keys are accepted. A signed-in browser session does not work for /api/v1, and keys can be created or revoked only from the signed-in web app, never through the API.
  • A missing, malformed, expired, or revoked key gets 401 unauthenticated.

A Key Acts as You ​

A key has no permissions of its own. It acts as its owner, with the owner's current organization and team roles, checked on every request:

  • The API returns exactly what you could see in the web app -- for an organization Member only their own teams, for Owners, Admins, and Implementers every team of the organization.
  • If you are removed from an organization or a team, your keys lose access to it immediately.
  • In a suspended organization, keys can still read, but every change is refused with 409 orgSuspended.
  • The platform operator's keys get no automatic access to customer data either -- like in the web app, they see an organization only if they are a member of it.

Scopes ​

The scopes of a key limit what it may do on top of your roles:

ScopeAllowsWho can create it
readReading everything your roles allowEveryone (Read only in the create dialog)
writeCreating and changing data as your roles allow; implies readEveryone (Read & write)
platformPlatform administrationPlatform operators (SUPERADMIN) only; valid for at most 90 days

A request whose key lacks the required scope gets 403 insufficientScope. A platform key stops working for platform administration as soon as its owner is no longer a platform operator. Reading needs read; creating, changing, and deleting data needs write (a Read & write key).

Your First Calls ​

Keep the key in an environment variable instead of typing it into every command. read -s does not echo the key and keeps it out of your shell history:

bash
read -rsp "API key: " EOSHUB_KEY && export EOSHUB_KEY   # paste your key, then Enter
export BASE="https://<your-host>/api/v1"

1. Who am I? GET /me returns your account, your organizations with your role in each, and the scopes of the key you are using:

bash
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/me"
json
{
  "user": { "id": "cmg4x…", "email": "jana@acme.com", "name": "Jana Nováková", "timezone": null },
  "orgs": [{ "id": "cmg1a…", "name": "Acme Corp", "status": "ACTIVE", "role": "OWNER" }],
  "scopes": ["read"],
  "key": { "id": "cmg9k…", "name": "n8n reports", "expiresAt": "2027-01-04T09:12:44.000Z" }
}

2. Teams of an organization. Take an organization id from the response:

bash
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/orgs/cmg1a…/teams"
json
{
  "data": [
    { "id": "cmg2b…", "name": "Leadership Team", "slug": "leadership", "isLeadership": true },
    { "id": "cmg2c…", "name": "Marketing", "slug": "marketing", "isLeadership": false }
  ],
  "nextCursor": null
}

3. Open to-dos of a team. To-Dos are called tasks in the API:

bash
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/teams/cmg2b…/tasks?completed=false"
json
{
  "data": [
    {
      "id": "cmg7t…",
      "teamId": "cmg2b…",
      "title": "Send the Q3 pricing proposal",
      "ownerId": "cmg4x…",
      "dueDate": "2026-10-13",
      "completed": false,
      "completedAt": null,
      "meetingId": "cmg6m…",
      "createdAt": "2026-10-06T08:41:02.000Z"
    }
  ],
  "nextCursor": null
}

TIP

The interactive reference shows the full response schema of every endpoint. Enter your key there to try calls straight from the browser.

Pagination ​

Lists return one page at a time:

json
{ "data": [ … ], "nextCursor": "eyJrIjoiMjAyNi0xMC0wNlQwODo0MTowMi4wMDBaIiwiaWQiOiJjbWc3dCJ9" }
ParameterDescription
limitPage size, 1--200 (default 50)
cursorThe nextCursor value from the previous page; omit it for the first page

When nextCursor is null, you have reached the last page.

The API uses cursor pagination: the cursor marks the last item you received, and the next page continues right after it. Unlike page numbers, it stays stable when items are added or deleted while you are paging -- you do not get duplicates or skip items. The trade-off is that you cannot jump to page N; you walk through the pages in order. Treat the cursor as an opaque string and always use the same filters for every page.

A few small lists -- organizations, teams of an organization, Scorecard metrics, and Accountability Chart seats -- always return everything at once with nextCursor: null.

Fetching every page:

bash
cursor=""
while :; do
  page=$(curl -s -H "Authorization: Bearer $EOSHUB_KEY" \
    "$BASE/teams/$TEAM_ID/tasks?limit=200${cursor:+&cursor=$cursor}")
  echo "$page" | jq -r '.data[] | "\(.dueDate)  \(.title)"'
  cursor=$(echo "$page" | jq -r '.nextCursor // empty')
  [ -z "$cursor" ] && break
done
js
const BASE = "https://<your-host>/api/v1"

async function* listAll(path, params = {}) {
  let cursor = null
  do {
    const url = new URL(BASE + path)
    for (const [k, v] of Object.entries({ ...params, limit: 200 })) url.searchParams.set(k, v)
    if (cursor) url.searchParams.set("cursor", cursor)

    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.EOSHUB_KEY}` } })
    const body = await res.json()
    if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`)

    yield* body.data
    cursor = body.nextCursor
  } while (cursor)
}

for await (const task of listAll(`/teams/${teamId}/tasks`, { completed: "false" })) {
  console.log(task.dueDate, task.title)
}

Errors ​

Errors use standard HTTP status codes and a JSON body with a machine-readable code:

json
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid",
    "issues": [
      {
        "origin": "number",
        "code": "too_big",
        "maximum": 200,
        "inclusive": true,
        "path": ["limit"],
        "message": "Too big: expected number to be <=200"
      }
    ]
  }
}
HTTPcodeMeaning
401unauthenticatedMissing, malformed, expired, or revoked API key
403forbiddenYour roles do not allow access to this organization or team, or do not allow this change
403insufficientScopeThe key's scopes do not allow this operation
404notFoundThe item does not exist, or you cannot see it
409orgSuspendedThe organization is suspended (read only) -- every change is refused
409ratingClosedThe rating window of the meeting is closed
422invalidInvalid parameters or body; for format errors issues lists the problems field by field (see Validation)
429rateLimitedToo many requests -- see Rate Limit
500internalUnexpected server error (no details are returned)

Items you cannot access. Lists under a team or an organization you have no access to -- for example /teams/{teamId}/tasks -- return 403 forbidden. Single items addressed by id -- /tasks/{id}, /issues/{id}, /goals/{id}, /meetings/{id}, /metrics/{id}/values -- return 404 notFound instead, so ids from other organizations reveal nothing.

Changes are different: a PATCH, PUT, or DELETE of an existing item you are not allowed to change -- for example a to-do of a team where you are a Viewer -- returns 403 forbidden. An id that does not exist returns 404 notFound.

Every response, including errors, carries an X-Request-Id header. Include it when you report a problem -- it lets the operator find the request in the server logs.

Rate Limit ​

Each API key may make 120 requests per minute. Above that, the API answers 429 rateLimited with a Retry-After header -- the number of seconds until the next minute starts. Wait that long, then continue. The limit is per key, so separate integrations should use separate keys.

TIP

Use limit=200 when you need whole lists -- one request per 200 items instead of four.

Data Formats ​

KindFormatExample
Field namescamelCasedueDate, isLeadership
IdsOpaque strings"cmg7t…"
Instants (created, completed, started …)ISO 8601, UTC"2026-10-06T14:30:00.000Z"
Calendar days (due dates, meeting day, metric week)YYYY-MM-DD, no time zone"2026-10-07"
QuartersYYYY-Qn"2026-Q3"
Enum valuesUPPER_CASEOPEN, ON_TRACK, COMPLETED
Missing valuesnull (fields are always present)"completedAt": null

Calendar days never shift between time zones -- a to-do due on 2026-10-07 is due on that day everywhere. Filters such as from, to, and quarter use the same formats.

A few fields are kept as they are stored in the app:

  • Metric goals keep their comparison operator, e.g. ">100000" or "<5", and metric values are strings.
  • V/TO fields are either plain text or structured JSON (lists, objects).
  • People Analyzer coreValuesScores and gwc are JSON objects.

The API version is part of the URL (/v1). New fields may be added to responses without a new version, so make your integration ignore fields it does not know.

EOS Terms in the API ​

The web app uses EOS terminology. The API uses neutral names, so integrations read naturally to developers who do not know EOS:

In EOS HubIn the API
Organization, teamorgs, teams
To-Dotasks
Issueissues
Rock, milestonegoals (milestones are embedded in each goal)
Scorecard measurable, weekly valuemetrics, metrics/{id}/values
L10 Meetingmeetings (with ratings, segues, headlines, messages)
V/TOvision
Accountability Chartorg-chart (seats)
People Analyzerpeople-reviews
Documentsdocuments

Endpoints ​

The endpoints in this section read data: they use GET and need the read scope. Paginated lists accept limit and cursor in addition to the filters shown. Endpoints that change data are listed in Writing Data.

Account and Organizations ​

EndpointReturnsFilters
/meYour account, organizations with your role, and the key's scopes--
/orgsYour organizations, with your role in each--
/orgs/{orgId}One organization--
/orgs/{orgId}/membersAll organization members with their organization role (paginated). Organization Owners and Admins only -- other members list the members of their teams via /teams/{teamId}/members--
/orgs/{orgId}/teamsTeams of the organization you can see--

Teams ​

EndpointReturnsFilters
/teams/{teamId}One team, including its meeting duration settings--
/teams/{teamId}/membersTeam members with their team role (paginated)--

Team Data ​

EndpointReturnsFilters
/teams/{teamId}/tasksTo-Dos (paginated)completed (true/false), ownerId
/tasks/{id}One to-do--
/teams/{teamId}/issuesIssues (paginated)status (OPEN, SOLVING, SOLVED), priority (HIGH, MEDIUM, LOW)
/issues/{id}One issue--
/teams/{teamId}/goalsRocks with their milestones (paginated)quarter (2026-Q3), status (ON_TRACK, OFF_TRACK, DONE), level (COMPANY, DEPARTMENT)
/orgs/{orgId}/goalsCompany Rocks of the organization, visible to all its members (paginated)quarter, status
/goals/{id}One Rock with its milestones (company Rocks are readable by every member of the organization)--
/teams/{teamId}/metricsScorecard measurables--
/metrics/{id}/valuesWeekly values of a measurable, oldest first (paginated)from, to (days, inclusive)
/teams/{teamId}/meetingsMeetings, newest first, each with its rating summary (paginated)status (SCHEDULED, IN_PROGRESS, COMPLETED)
/meetings/{id}One meeting with notes, ratings, segues, headlines, cascading messages, and the rating window--
/teams/{teamId}/visionThe team's V/TO (404 if the team has none yet)--
/teams/{teamId}/org-chartAccountability Chart seats; parentId links each seat to the one above it--
/teams/{teamId}/people-reviewsPeople Analyzer entries (paginated)quarter (2026-Q3)
/teams/{teamId}/documentsDocuments and links (paginated)--

Meeting ratings follow the same rules as in the app: the rating.average excludes absent and unrated members (see L10 Meetings), and ratingWindow tells whether team members may still rate.

Writing Data ​

To-dos, issues, Rocks with their milestones, and Scorecard measurables with their weekly values can be created, changed, and deleted through the API. Every change needs a key with the write scope (Read & write in the create dialog) -- a Read only key gets 403 insufficientScope.

Methods ​

MethodUseSuccess response
POSTCreate an item201 Created with the created item
PATCHPartial update -- send only the fields you want to change; omitted fields keep their values200 OK with the updated item
PUTSet a metric's value for one week (create or replace) -- idempotent, sending the same request again changes nothing200 OK with the saved value
DELETEDelete an item204 No Content, no body

Send the body as JSON with the header Content-Type: application/json. The responses have the same shape as the corresponding GET -- a created to-do looks exactly like one from GET /tasks/{id}.

A PATCH body must change at least one field: an empty body {} is refused with 422 invalid ("Nothing to update"). Fields the API does not know are ignored -- a body with only unknown fields counts as empty.

Permissions ​

Writes follow the same rules as the web app (see Roles & Permissions). Your role in the item's team decides; organization Owners, Admins, and Implementers act as team Admins in every team.

OperationAllowed for
Create, edit, complete, and reopen to-dos (anyone's, not only your own)Team Members and above
Create and edit issues, change their statusTeam Members and above
Enter weekly metric valuesTeam Members and above
Change a Rock's status; add, edit, complete, and delete its milestonesThe Rock's owner (Member or above) and team Admins
Create Rocks; change their title, description, owner, quarter, or levelTeam Admins and Owners
Add, edit, and reorder metricsTeam Admins and Owners
Delete to-dos, issues, Rocks, and metricsTeam Admins and Owners

For example: a Viewer cannot change anything; a Member can complete a colleague's to-do but cannot delete it; the owner of a Rock may set its status to OFF_TRACK and tick off its milestones, but may not rename it, give it to someone else, or move it to another quarter. A refused change returns 403 forbidden.

More rules:

  • Owners must be team members. An ownerId you send for a to-do, Rock, or metric must be a member of the item's team (the ids are in GET /teams/{teamId}/members); otherwise the API answers 422 invalid. Other edits work even if the current owner has left the team.
  • One request, one change. A PATCH with several fields (for example a new title and a status) is applied completely or not at all.
  • Small bodies. Request bodies are limited to 64 KB (413 otherwise).
  • Meetings must belong to the team. The optional meetingId of a new to-do or issue must be a meeting of the same team.
  • Suspended organizations are read only. Every change in a suspended organization returns 409 orgSuspended.
  • Calendar days (dueDate, the metric week) accept only YYYY-MM-DD. An instant such as 2026-10-14T00:00:00Z or a non-existent day such as 2026-02-30 is refused with 422 invalid.
  • Text is trimmed. Leading and trailing spaces are removed; a title that is empty after trimming is invalid.

Validation ​

Invalid input returns 422 invalid and changes nothing. There are two kinds:

  • Format errors -- a missing required field, a wrong type (for example a number where a string is expected), an unknown enum value, a malformed day or quarter, an empty PATCH body. The response lists each problem in issues with the field's path.
  • Rule violations -- a text over its length limit (see the tables below), an owner who is not a team member, a meeting of another team, a company Rock in a non-leadership team. The response has the code invalid but no issues.

To-Dos (tasks) ​

EndpointPurposeResponse
POST /teams/{teamId}/tasksCreate a to-do201 Task
PATCH /tasks/{id}Change, complete, or reopen a to-do200 Task
DELETE /tasks/{id}Delete a to-do204

Create body:

FieldTypeRequiredNotes
titlestringyes1--500 characters
ownerIdstringyesA member of the team
dueDatedayyesYYYY-MM-DD
meetingIdstringnoThe meeting of the same team the to-do came from

Update body -- all fields optional, at least one required:

FieldTypeNotes
titlestring1--500 characters
ownerIdstringA member of the team
dueDatedayYYYY-MM-DD
completedbooleantrue completes the to-do and sets completedAt; false reopens it and clears completedAt

Issues ​

EndpointPurposeResponse
POST /teams/{teamId}/issuesCreate an issue201 Issue
PATCH /issues/{id}Change an issue or its status200 Issue
DELETE /issues/{id}Delete an issue204

Create body:

FieldTypeRequiredNotes
titlestringyes1--500 characters
descriptionstringnoUp to 5,000 characters
priorityHIGH | MEDIUM | LOWnoDefault MEDIUM
meetingIdstringnoThe meeting of the same team the issue came from

New issues start with status OPEN; you are recorded as createdById.

Update body -- all fields optional, at least one required:

FieldTypeNotes
titlestring1--500 characters
descriptionstringUp to 5,000 characters; "" clears it
priorityHIGH | MEDIUM | LOW
statusOPEN | SOLVING | SOLVEDSOLVED sets resolvedAt to now; OPEN and SOLVING clear it

Rocks (goals) and Milestones ​

EndpointPurposeResponse
POST /teams/{teamId}/goalsCreate a Rock201 Goal
PATCH /goals/{id}Change a Rock or report its status200 Goal
DELETE /goals/{id}Delete a Rock with its milestones204
POST /goals/{id}/milestonesAdd a milestone to a Rock201 Milestone
PATCH /milestones/{id}Change, complete, or reopen a milestone200 Milestone
DELETE /milestones/{id}Delete a milestone204

Milestone ids are in the milestones array of each Rock (GET /goals/{id}).

Create body for a Rock:

FieldTypeRequiredNotes
titlestringyes1--300 characters
descriptionstringnoUp to 5,000 characters
ownerIdstringyesA member of the team
quarterstringyesYYYY-Qn, e.g. "2026-Q4" (years 2000--2100)
levelCOMPANY | DEPARTMENTnoDefault DEPARTMENT. COMPANY only in a leadership team, otherwise 422 invalid

New Rocks start with status ON_TRACK and no milestones.

Update body -- all fields optional, at least one required:

FieldTypeNotes
titlestring1--300 characters
descriptionstringUp to 5,000 characters; "" clears it
ownerIdstringA member of the team
quarterstringYYYY-Qn
levelCOMPANY | DEPARTMENTCOMPANY only in a leadership team
statusON_TRACK | OFF_TRACK | DONEThe only field the Rock's owner may change; all other fields need a team Admin

Milestone bodies:

FieldTypeCreateUpdateNotes
titlestringrequiredoptional1--300 characters
dueDateday or nulloptional, default nulloptionalYYYY-MM-DD; null removes the due date
completedboolean--optionaltrue completes, false reopens. New milestones start not completed

Scorecard Metrics (metrics) ​

EndpointPurposeResponse
POST /teams/{teamId}/metricsAdd a metric at the end of the team's Scorecard201 Metric
PATCH /metrics/{id}Change or reorder a metric200 Metric
DELETE /metrics/{id}Delete a metric with all its values204
PUT /metrics/{id}/values/{week}Set the value of one week200 MetricValue

Create body:

FieldTypeRequiredNotes
titlestringyes1--200 characters
goalstringyes1--200 characters. A target with a comparison operator >, >=, <, <=, or =, e.g. ">=10" or "<5"; a bare number such as "10" means at least (>=)
ownerIdstringyesA member of the team
unitstringnoUp to 20 characters, e.g. "€" or "%"
descriptionstringnoUp to 5,000 characters

Update body -- all fields optional, at least one required:

FieldTypeNotes
title, goal, ownerIdstringAs when creating
unit, descriptionstringAs when creating; "" clears the field
direction"up" | "down"Moves the metric one place up or down in the Scorecard. At the top or bottom nothing moves

Weekly value -- PUT /metrics/{id}/values/{week}:

  • {week} is the Monday that starts the week, as YYYY-MM-DD. Any other day returns 422 invalid ("Weeks start on Monday").
  • The request is an upsert: it creates the week's value or replaces it. Repeating it gives the same result. enteredById keeps the person who entered the value first.
FieldTypeRequiredNotes
valuestringyesUp to 50 characters. Send numbers as strings: "12", not 12
onTrackbooleannoWhether the week met the goal. If omitted, it is computed from the goal (goal ">=10", value "12" → true; value "8" → false). When the value can't be judged -- text, or an ambiguous number such as "1.234,5" -- onTrack is required (422 otherwise)

Numbers may use a decimal comma or dot ("3,5" and "3.5" are 3.5), thousands groups ("1.000" and "1,000" are 1000), spaces and a trailing %. A number that mixes both separators ("1.234,5") is not guessed.

Examples ​

The examples use the variables from Your First Calls plus ids you got from earlier responses.

Create a to-do:

bash
curl -s -X POST "$BASE/teams/$TEAM_ID/tasks" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Call the new supplier", "ownerId": "cmg4x…", "dueDate": "2026-10-14"}'
json
{
  "id": "cmg7u…",
  "teamId": "cmg2b…",
  "title": "Call the new supplier",
  "ownerId": "cmg4x…",
  "dueDate": "2026-10-14",
  "completed": false,
  "completedAt": null,
  "meetingId": null,
  "createdAt": "2026-10-07T09:15:31.000Z"
}

Complete it:

bash
curl -s -X PATCH "$BASE/tasks/cmg7u…" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

The response is the to-do with "completed": true and completedAt set. Send {"completed": false} to reopen it.

Enter this week's value of a metric. Weeks are identified by their Monday:

bash
MONDAY=$(date -d "-$(( $(date +%u) - 1 )) days" +%F)   # GNU date; on macOS: date -v-$(( $(date +%u) - 1 ))d +%F
curl -s -X PUT "$BASE/metrics/$METRIC_ID/values/$MONDAY" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": "12"}'
json
{ "week": "2026-10-05", "value": "12", "onTrack": true, "enteredById": "cmg4x…" }

With the goal ">=10" the value "12" is on track, so onTrack is true. Running the same command again changes nothing; sending another value replaces the week's value.

Mark an issue as solved:

bash
curl -s -X PATCH "$BASE/issues/cmg8i…" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "SOLVED"}'

The response is the issue with "status": "SOLVED" and resolvedAt set. Moving it back to OPEN or SOLVING clears resolvedAt.

Interactive Reference ​

Open https://<your-host>/api/v1/docs for the interactive API reference. It lists every endpoint with its parameters and full response schema, and you can enter your API key to send real requests from the browser.

The same description is available as an OpenAPI 3.1 document at https://<your-host>/api/v1/openapi.json. Import it into Postman, Insomnia, or a client code generator.

Built with VitePress