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.

Scope

Everything you can do in the web app is available through the API as well:

  • Team data -- read and write to-dos, issues, Rocks with their milestones, Scorecard measurables with their weekly values, meetings with their ratings, segues, headlines, and cascading messages, the V/TO, People Analyzer entries, and document links (the Accountability Chart is read-only) -- see Endpoints and Writing Data.
  • Organization and team administration -- organization settings, members, teams, and team members -- see Organization Administration.
  • Platform administration -- organizations, accounts, API keys of users, and AI settings, for the platform operator -- see Platform Administration.

The only exception is managing your own API keys: you create and revoke them only in the signed-in web app (see API Keys).

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.
  • AI assistants connect through the MCP server instead. Their OAuth tokens work only there, never on /api/v1.

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 a user's API keys on the platform -- 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
409conflictAn externalId already names an item of another team or meeting, or two requests import the same externalId at once (see Importing History)
409ratingClosedThe meeting cannot be rated now: it has not started yet (SCHEDULED -- applies to everyone, team Admins included), or the rating window for your own rating has closed (team members who are not team Admins). See Meetings
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)

Reasons. Where the code alone doesn't say which check failed, error.details.reason does:

json
{ "error": { "code": "invalid", "message": "A scorecard week is its Monday (YYYY-MM-DD)", "details": { "reason": "INVALID_WEEK" } } }
reasoncodeMeaning
INVALID_WEEKinvalidA Scorecard week that is not a Monday
OWNER_NOT_MEMBERinvalidThe owner (or author) is not a member of the team
INVALID_RATINGinvalidA meeting rating that is not a whole number from 1 to 10
NOT_TEAM_MEETINGinvalidThe meetingId is a meeting of another team
BACKFILL_FORBIDDENforbiddenWriting history (a past date, another author) needs team Admin rights

New reasons may be added; treat an unknown one like a missing one.

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 each have a fixed shape: a list of texts, an object, or text -- the 3-year picture and the 1-year plan are text or an object (see V/TO).
  • People Analyzer coreValues (ratings "+", "+/-", "-") and gwc (true, false, null) 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 (drafts and versions)
People Analyzerpeople-reviews
Documentsdocuments

Endpoints ​

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

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--
/orgs/{orgId}/visionThe company vision, shared by all teams of the organization (see V/TO)--

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, meetingId
/tasks/{id}One to-do--
/teams/{teamId}/issuesIssues (paginated)status (OPEN, SOLVING, SOLVED), priority (HIGH, MEDIUM, LOW), longTerm (true/false), meetingId
/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 (with comments), segues, headlines, cascading messages, guests, the ids of the to-dos and issues created in it, and the rating window--
/teams/{teamId}/visionThe team's V/TO: the company vision, the team's 1-year plan, the current quarter's Rocks and the open long-term issues (see V/TO)--
/teams/{teamId}/org-chartThe Accountability Chart that applies to the team; ?version=n for a published version--
/teams/{teamId}/people-reviewsPeople Analyzer entries: your own, or everyone's for the team's Admins (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.

Platform ​

For the platform operator, with a platform key -- see Platform Administration. These endpoints return no organization data.

EndpointReturnsFilters
/platform/orgsAll organizations with their status and the number of members and teams--
/platform/usersAll accounts with the number of organizations and active API keys, oldest first (paginated)--
/platform/users/{id}/api-keysOne account's API keys, newest first -- details only, never the key itself--
/platform/aiAI providers (API keys masked) and the custom system prompts--
/platform/feedbackFeedback users sent, newest first (PATCH/DELETE /platform/feedback/{id} set the status and internal note, or delete)kind (BUG, IDEA), status (NEW, IN_PROGRESS, DONE, DISMISSED)

Any user can send feedback to the platform's operators with POST /feedback (kind, message, expected, orgId; a write key, at most 20 an hour).

Writing Data ​

To-dos, issues, Rocks with their milestones, Scorecard measurables with their weekly values, meetings, the V/TO, People Analyzer entries, and documents can be written 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. Organization settings, members, and teams are changed with the endpoints in Organization Administration.

Methods ​

MethodUseSuccess response
POSTCreate an item201 Created with the created item (POST /messages/acknowledge: 204 No Content). With an externalId sent before: 200 OK with the item created then (see Importing History)
PATCHPartial update -- send only the fields you want to change; omitted fields keep their values200 OK with the updated item
PUTCreate or replace: a metric's value for one week, a meeting rating, your People Analyzer review -- idempotent, sending the same request again changes nothing200 OK with the saved item
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. The exceptions are the meeting bodies (POST /teams/{teamId}/meetings, PATCH /meetings/{id}, PUT /meetings/{id}/ratings/{userId}): they accept only the shapes listed in Meetings, and an unknown or extra field is refused with 422 invalid.

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 status, move them between short-term and long-termTeam 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
Schedule and record (backfill) meetings, start and end them, correct a completed meeting's timesTeam Admins and Owners
Rate a meeting yourselfTeam Members and above who are on the team -- not Implementers, and not organization Owners or Admins who are not members of the team. Inside the rating window, unless you are a team Admin
Set or clear a member's rating, mark members absentTeam Admins and Owners
Add segues, headlines, and cascading messagesTeam Members and above
Acknowledge (mark as read) cascading messagesAny member of the message's organization
Edit the team's V/TO (1-year plan, clearing the archive)Team Admins and Owners
Edit the company vision (PATCH /orgs/{orgId}/vision)Organization Owners and Admins, Implementers, and the Admins and Owners of a leadership team
Write People Analyzer reviews (you are always the evaluator)Team Members and above
Add document linksTeam Members and above
Delete to-dos, issues, Rocks, metrics, cascading messages, and documentsTeam Admins and Owners
Write history: createdAt, completedAt, resolvedAt, another author (userId, createdById)Team Admins and Owners (403 forbidden, reason BACKFILL_FORBIDDEN, otherwise)
Set a meeting's guestsTeam Admins and Owners
Undo an import (DELETE /orgs/{orgId}/import-batches/{batchId})Organization Owners and Admins

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; an Implementer runs meetings and records the members' ratings like a team Admin but never rates a meeting themselves. 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 with reason OWNER_NOT_MEMBER. 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 (reason NOT_TEAM_MEETING otherwise).
  • Suspended organizations are read only. Every change in a suspended organization returns 409 orgSuspended.
  • Calendar days (dueDate, the metric week, the meeting date) 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.
  • Instants (the meeting startedAt, the backfilled createdAt, completedAt, resolvedAt) need ISO 8601 with a time zone: 2026-10-05T09:00:00+02:00 or 2026-10-05T07:00:00Z. A time without a zone, such as 2026-10-05T09:00:00, 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, a meeting start in the future, a meeting status step backwards, a rating that is not a whole number from 1 to 10. 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
completedbooleannotrue creates it done (completedAt now unless given)
completedAtinstantnoWhen it was done -- needs completed: true; not in the future. Team Admins only
createdAtinstantnoWhen it was created; not in the future. Team Admins only
externalId, importBatchIdstringnoSee Importing History

GET /teams/{teamId}/tasks?meetingId=… lists the to-dos created in one meeting.

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
longTermbooleannotrue puts the issue on the long-term list (the V/TO's, for quarterly planning) instead of the short-term list of the weekly meeting. Default false
meetingIdstringnoThe meeting of the same team the issue came from
statusOPEN | SOLVING | SOLVEDnoDefault OPEN; SOLVED sets resolvedAt (now unless given)
createdByIdstringnoWho raised it, when not you -- a member of the team. Team Admins only
createdAtinstantnoWhen it was raised; not in the future. Team Admins only
resolvedAtinstantnoWhen it was solved -- needs status: SOLVED; not in the future. Team Admins only
externalId, importBatchIdstringnoSee Importing History

New issues start with status OPEN unless you send another; you are recorded as createdById unless a team Admin names someone else. GET /teams/{teamId}/issues?meetingId=… lists the issues raised in one meeting. Every Issue in a response has longTerm; the weekly meeting's IDS works only with issues where it is false.

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
longTermbooleanMoves the issue to the long-term (true) or short-term (false) list

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
GET /goals/{id}/reportsThe Rock's weekly reports, oldest first200 list of GoalReport
PUT /goals/{id}/reports/{week}Report a week (its Monday): status (ON_TRACK, OFF_TRACK, DONE, UNKNOWN), note200 GoalReport
DELETE /goals/{id}/reports/{week}Remove a week's report204
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}).

Weekly reports. A Rock has one report per week (week = the Monday). The week must belong to the Rock's quarter and lie at most one week ahead (else 422 with reason INVALID_WEEK). The Rock's status follows its newest report that isn't UNKNOWN (UNKNOWN = "?", nobody knew). PATCH /goals/{id} with status (and optionally note) is the report of the reporting week: this week, or next week once the team's meeting of this week is completed -- when that week belongs to the Rock's quarter. Reports are progress: the owner or a team Admin (reportedBy says who entered it).

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
POST /metrics/{id}/values/batchSet the values of many weeks (up to 500, body at most 64 KB), each saved on its own200 per-week results
DELETE /metrics/{id}/values/{week}Clear the value of one week204 (404 if the week has none)

Create body:

FieldTypeRequiredNotes
titlestringyes1--200 characters
goalstring | nullnoUp to 200 characters. A target with a comparison operator >, >=, <, <=, or =, e.g. ">=10" or "<5", or a range "90-110" (inclusive); a bare number such as "10" means at least (>=). null or left out: a tracking-only metric -- its values are recorded but not judged (onTrack: null)
ownerIdstringyesA member of the team
unitstringnoUp to 20 characters, e.g. "€" or "%"
descriptionstringnoUp to 5,000 characters
externalId, importBatchIdstringnoSee Importing History

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

FieldTypeNotes
title, ownerIdstringAs when creating
goalstring | nullAs when creating; null makes the metric tracking-only
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 with reason INVALID_WEEK.
  • 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.
  • In the app, values entered during an L10 meeting land in the week the Scorecard shows as current in your time zone (from your profile, otherwise the organization's).
FieldTypeRequiredNotes
valuestringyesUp to 50 characters. Send numbers as strings: "12", not 12
onTrackbooleannoLeave it out whenever the value can be compared with the goal: the goal decides (goal ">=10", value "12" → true; value "8" → false), and an onTrack that contradicts it is refused with 422. Only when the value can't be judged -- text, or an ambiguous number such as "1.234,5" -- onTrack is required (422 otherwise). A tracking-only metric takes values without it
notestringnoUp to 500 characters: a note on the week's value. Replaces the stored note; "" clears it; left out keeps it
importBatchIdstringnoSee Importing History

Many weeks at once -- POST /metrics/{id}/values/batch:

json
{
  "values": [
    { "week": "2026-03-02", "value": "11" },
    { "week": "2026-03-09", "value": "about 12", "onTrack": true, "note": "estimate" }
  ],
  "importBatchId": "sw-history-1"
}

Each item takes the fields of the single-week body plus its week. Every week is saved on its own, so one bad week does not stop the others. The response lists each week in order:

json
{ "results": [
  { "week": "2026-03-02", "status": "updated" },
  { "week": "2026-03-09", "status": "created" },
  { "week": "2026-03-10", "status": "error", "error": { "code": "invalid", "message": "A scorecard week is its Monday (YYYY-MM-DD)", "details": { "reason": "INVALID_WEEK" } } }
] }

A malformed body (not a day, more than 500 items) is refused as a whole with 422.

Reads (GET /metrics/{id}/values) return note and report onTrack as the current goal judges the value (null for a tracking-only metric), so changing a goal re-evaluates the history; a recorded onTrack only counts for values the goal can't judge.

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.

Meetings ​

EndpointPurposeResponse
POST /teams/{teamId}/meetingsSchedule a meeting, or record one that already happened201 Meeting
PATCH /meetings/{id}Start or end a meeting, correct a completed meeting's times, or write its notes ({ "notes": "…" })200 Meeting
POST /meetings/{id}/reveal-ratingsReveal a running meeting's ratings (facilitators)204
GET /meetings/{id}/recapRecap of a completed meeting: rating, new to-dos, issues, headlines, messages, notes; also attendance (members present/absent with their ratings, guests), scorecard (the meeting's week and the three before), goals (the quarter's goals as reported that week), todoReview (done of total, and those not done), segues and agenda (each step's time box and the seconds it took)200 MeetingRecap
PUT /meetings/{id}/ratings/meRate the meeting yourself200 Rating
PUT /meetings/{id}/ratings/{userId}Set or clear a member's rating (with a comment), or mark them absent200 Rating, or 204 when cleared
PUT /meetings/{id}/guestsReplace who else attended (people outside the team)200 { data: MeetingGuest[] }
POST /meetings/{id}/seguesAdd a segue (good news)201 Segue
POST /meetings/{id}/headlinesAdd a customer, employee or company headline; userId (whose it is): any team member in a running meeting, otherwise team Admins201 Headline
POST /meetings/{id}/messagesAdd a cascading message201 CascadingMessage
DELETE /meetings/{id}Delete a meeting with its segues, headlines, messages, ratings and guests (team Admins; to-dos and issues raised in it stay)204
PATCH /segues/{id}Correct a segue: { "content" }200 Segue
DELETE /segues/{id}Delete a segue204
PATCH /headlines/{id}Correct a headline: content, type, userId (whose it is)200 Headline
DELETE /headlines/{id}Delete a headline204
PATCH /messages/{id}Correct a cascading message: { "content" }200 CascadingMessage
DELETE /messages/{id}Delete a cascading message204
POST /messages/acknowledgeMark cascading messages as read204

While a meeting runs, its ratings are hidden until a facilitator reveals them (ratingsHidden: true): you get your own rating, the others' come as rating: 0, absent: false (rated, hidden), and the average is null. Meanwhile a facilitator can mark members absent or present again, but setting or clearing someone else's number returns 422 invalid.

The Meeting in a response has the same shape as in GET /teams/{teamId}/meetings, including the rating summary. GET /meetings/{id} adds the notes, the ratings with their comment (hidden like the number until the reveal), segues, headlines, messages, guests, and taskIds and issueIds -- the to-dos and issues created in the meeting. To-dos and issues raised in a meeting are created with the to-do and issue endpoints -- pass the meeting's id as meetingId (a meeting of the same team).

Create -- send exactly one of the two shapes, never fields of both:

ShapeFieldTypeNotes
ScheduledatedayThe day of the meeting, YYYY-MM-DD. The meeting starts as SCHEDULED
Record a past meetingstartedAtinstantWhen the meeting started, with a time zone offset. Not in the future
durationMinintegerLength in whole minutes, 1--600
Either shapeexternalId, importBatchIdstringOptional; see Importing History

A recorded (backfilled) meeting is created as COMPLETED, with endedAt = startedAt + durationMin. Its date is the day of startedAt in your time zone (from your profile, otherwise the organization's) -- a meeting started at 2026-10-05T23:30:00Z falls on 2026-10-06 in Central Europe.

Update -- again exactly one of two shapes:

ShapeFieldTypeNotes
Advance the statusstatusIN_PROGRESS | COMPLETEDOnly forward, one step at a time: SCHEDULED → IN_PROGRESS → COMPLETED. Any other step (back, the same status, or SCHEDULED straight to COMPLETED) returns 422 invalid
Correct the timesstartedAtinstantCompleted meetings only (422 otherwise). Not in the future
durationMinintegerWhole minutes, 1--600

Starting a meeting (IN_PROGRESS) sets startedAt to now. Completing it sets endedAt to now and records durationMin -- the minutes since the start. Correcting the times recomputes endedAt, and the meeting's date follows the new start, as when recording.

Ratings. Ratings are whole numbers from 1 to 10, as in the app (see L10 Meetings).

  • PUT /meetings/{id}/ratings/me with { "rating": 8 } saves your own rating and marks you present. You must be a member of the meeting's team. Implementers never rate, and organization Owners and Admins rate only in teams they belong to (403 forbidden otherwise).
  • Rating window. A meeting that has not started yet (SCHEDULED) cannot be rated by anyone -- 409 ratingClosed. Team members may rate while the meeting is in progress and after it ends, until the team's next meeting starts, but at most 7 days. For a recorded (backfilled) meeting the 7 days count from when it was entered, not from when it took place. After that, members get 409 ratingClosed. Team Admins may rate at any time. GET /meetings/{id} shows the window as ratingWindow.open and ratingWindow.closesAt.
  • PUT /meetings/{id}/ratings/{userId} is for team Admins (and Implementers) and is not limited by the window, only by SCHEDULED. The userId must be a member of the team (422 invalid otherwise). A rating outside 1--10 returns 422 invalid with reason INVALID_RATING. Each shape below may also carry comment (up to 500 characters: why this number; "" clears it) and importBatchId.

The body must be exactly one of these shapes:

BodyEffectResponse
{ "rating": 8 }Sets the member's rating (1--10) and marks them present200 Rating
{ "rating": 0 }Clears the member's rating: they count as present but not rated204 No Content
{ "absent": true }Marks the member absent and stores the rating 0. Absent members do not count toward the average200 Rating
{ "rating": 8, "absent": false }The same as { "rating": 8 } with absent stated explicitly ("rating": 0 clears)200 Rating, or 204

There are no defaults: an empty body {}, { "absent": false } without a rating, an unknown field, or a rating together with "absent": true returns 422 invalid.

Segues, headlines, and cascading messages. Bodies:

EndpointFieldTypeNotes
segues, messagescontentstring1--1,000 characters
headlinestypeCUSTOMER | EMPLOYEE | COMPANYCOMPANY: company news
contentstring1--1,000 characters
all threeuserIdstringOptional: who said it, when not you -- a member of the team. Team Admins only
all threeexternalId, importBatchIdstringOptional; see Importing History

You are recorded as the author (userId) unless a team Admin names someone else.

Guests -- PUT /meetings/{id}/guests with { "guests": [{ "name": "Eva Coach", "email": "eva@example.com" }, { "name": "Ivan", "userId": "…" }] } replaces the list of people outside the team who attended (up to 50). email and userId are optional; a userId must be a member of the organization. { "guests": [] } clears the list. Team Admins only. Segues, headlines and cascading messages are corrected or deleted by their author -- while the meeting runs and afterwards while the member rating window is open -- or by a team Admin at any time (others get 403). Headlines are the team's shared notes while the meeting runs: then any team member may change or delete any of them. Their ids are in the segues, headlines and messages arrays of GET /meetings/{id}. A content change to a completed meeting that was run live sets the meeting's correctedAt and correctedBy ("Edited"); the recap has them as corrected. Ratings and attendance don't count, and meetings recorded afterwards (backfilled, imported) are never marked.

Acknowledge -- POST /messages/acknowledge with { "ids": ["…", "…"] } (1--500 ids) marks the messages as read for you. Any member of the message's organization may do so, not only the members of the team that wrote it. Repeating it changes nothing, and ids that do not exist are skipped. If any of the messages belongs to an organization you are not a member of, the request returns 403 forbidden and acknowledges nothing.

V/TO (vision) ​

The V/TO has two parts. The company vision -- core values, core focus, 10-year target, marketing strategy, and 3-year picture -- is one per organization and shared by all its teams. The traction belongs to each team: its 1-year plan, its Rocks of the current quarter, and its long-term issues. The Rocks and issues are read live from their own endpoints; on the V/TO you only write the 1-year plan.

EndpointPurposeResponse
GET /orgs/{orgId}/visionThe company vision200 CompanyVision
PATCH /orgs/{orgId}/visionSet one field of the company vision200 CompanyVision
GET /teams/{teamId}/visionThe team's V/TO: company vision and team traction200 Vision
PATCH /teams/{teamId}/visionSet the team's 1-year plan, or clear the archive200 Vision
POST /teams/{teamId}/visionCreate the team's traction record if it does not exist yet (no body)201 Vision

CompanyVision -- the same object on both organization endpoints and inside the team's V/TO:

FieldTypeNotes
orgIdstring
coreValueslist of texts
coreFocusobjectTexts, e.g. purpose, niche
tenYearTargettext
marketingStrategyobjectTexts or lists of texts, e.g. targetMarket, threeUniques, provenProcess, guarantee
threeYearPicturetext or objectAn object of texts or lists of texts, e.g. revenue, profit, measurables
peopleBarnumberPeople Analyzer: how many +/- a right person may have on the core values (a - never passes); default 1
updatedAtinstant or nullnull until someone writes the vision

Fields nobody has filled in are "". To clear a field, PATCH it with ""; a list or object field also takes [] or {}. The app's forms keep object entries under names they don't know and show them under their own name.

Vision -- a team's V/TO:

FieldTypeNotes
teamIdstring
companyVisionCompanyVisionThe organization's vision, the same for every team
oneYearPlantext or objectAn object of texts or lists of texts, e.g. revenue, profit, goals
quarterquarterThe current quarter in your time zone, e.g. "2026-Q4"
goalslist of GoalThe quarter's Rocks, live, in the same shape as GET /goals/{id}: for the leadership team the organization's COMPANY Rocks, for other teams their own DEPARTMENT Rocks
longTermIssueslist of IssueThe team's open (OPEN, SOLVING) long-term issues, live, in the same shape as GET /issues/{id}
archiveobjectquarterlyRocks and issuesList: the hand-written lists from before the Rocks and issues were shown live -- a list of texts, or text for older free-form entries; "" when there are none
updatedAtinstant or nullWhen the 1-year plan or the archive last changed; null if the team never saved anything

Every team has a V/TO: GET /teams/{teamId}/vision never returns 404 for a team you can see -- a team that has not written anything gets empty fields.

Breaking change in v1.8

GET /teams/{teamId}/vision used to return the eight V/TO fields flat and 404 for a team without a V/TO. The vision fields are now in companyVision, the Rocks and issues come live in goals and longTermIssues, and the old quarterlyRocks and issuesList texts are in archive. The change was made while the API was available only on the dev environment.

Who may. Every member of the organization can read the company vision; for an organization you are not a member of, GET /orgs/{orgId}/vision returns 403 forbidden like the other /orgs endpoints. PATCH /orgs/{orgId}/vision is for organization Owners and Admins, Implementers, and the Admins and Owners of a leadership team; others get 403 forbidden. The team's 1-year plan and archive are changed by the team's Admins and Owners. Changes need a key with the write scope and return 409 orgSuspended in a suspended organization.

Company vision. Each PATCH /orgs/{orgId}/vision sets one field:

FieldTypeNotes
fieldstringOne of coreValues, coreFocus, tenYearTarget, marketingStrategy, threeYearPicture, peopleBar
valuedepends on fieldSee the next table. Up to 20,000 characters as stored
renamesobjectOptional, coreValues only: renamed values, old text → new text, e.g. {"Own it": "Own the outcome"}. The People Analyzer ratings given under the old text move to the new one in every team of the organization -- for pairs where the old text leaves the list and the new one joins it. Not available to AI assistants, nor is setting peopleBar. Without it, ratings of a value written differently stay under the old text and stop counting

Team V/TO. Each PATCH /teams/{teamId}/vision also sets one field, with field one of oneYearPlan, quarterlyRocks, or issuesList. quarterlyRocks and issuesList are the archive: they only accept an empty value ("" or []), which clears that list; anything else returns 422 invalid -- add Rocks and issues with their own endpoints. The company fields are not accepted here (422 invalid); use PATCH /orgs/{orgId}/vision. PATCH creates the team's traction record if it is missing. POST on a team that already has one changes nothing and returns the current V/TO, still with 201.

Each field takes one shape:

Fieldsvalue
coreValuesA list of texts: at most 100 items, each 1--1,000 characters (trimmed)
coreFocus, marketingStrategyAn object with at most 30 keys (1--100 characters each); each value is a text (up to 5,000 characters) or a list of texts as above
threeYearPicture, oneYearPlanText, stored as sent (not trimmed), or an object as above
tenYearTargetText, stored as sent (not trimmed)
peopleBarA whole number 0--10
quarterlyRocks, issuesListOnly "" or [] (clears the archive)

An empty value clears a field: "" any field, [] or {} a list or object field. Anything else -- other text for a list or object field, a list or object for tenYearTarget, a number, a boolean, null -- returns 422 invalid. Example bodies:

json
{ "field": "coreValues", "value": ["Customer first", "Own it", "Keep it simple"] }
{ "field": "marketingStrategy", "value": { "targetMarket": "B2B companies with 10-250 people", "threeUniques": ["Built for EOS", "L10 meetings", "Works on a phone"] } }
{ "field": "tenYearTarget", "value": "1,000 companies run on EOS Hub" }
{ "field": "threeYearPicture", "value": { "revenue": "€5M", "profit": "€800k", "measurables": ["300 customers", "40 people"] } }

and for the team (PATCH /teams/{teamId}/vision):

json
{ "field": "oneYearPlan", "value": { "revenue": "€2M", "profit": "€250k", "goals": ["Launch the mobile app", "Hire 5 engineers"] } }
{ "field": "quarterlyRocks", "value": "" }

The response is the whole CompanyVision or Vision, as from the matching GET: list and object fields come back parsed, text fields always as text -- text that looks like JSON (for example "[1, 2]") stays a string. A list or object field that still holds older free text from the app is returned as that text.

Accountability Chart (org-chart) ​

Everything works on the chart that applies to the team: the company chart when the organization keeps one chart (then only its editors -- org owners and admins, Implementers, leadership team admins -- change it), otherwise the team's own (its admins). Changes go through a draft and become a version when published, as in the app.

EndpointPurposeResponse
GET /teams/{teamId}/org-chartThe chart that applies to the team. Optional ?version=n reads a published version instead of the current one200 OrgChart
GET /orgs/{orgId}/org-chartThe organization's chart (only with one chart for the organization; 404 with a chart per team)200 OrgChart
POST /teams/{teamId}/org-chart/draftOpen a draft: {"from": "current"} (default, a copy of the latest version), "leadership" (the leadership team's own chart, company chart only) or "empty". Returns the open draft if there is one201 ChartDraft
GET /teams/{teamId}/org-chart/draftThe open draft (404 when there is none)200 ChartDraft
PUT /teams/{teamId}/org-chart/draftSave the whole draft: {"revision": 3, "seats": [...]}200 { revision, updatedAt }
DELETE /teams/{teamId}/org-chart/draftDiscard the draft204
POST /teams/{teamId}/org-chart/draft/publishPublish it: {"effectiveFrom": "2026-11-01", "note": "…"}. A future day schedules the version200 OrgChart
POST /teams/{teamId}/org-chart/versions/{number}/withdrawTake back the latest version before its day: it becomes the draft again (422 while another draft is open)200 OrgChart

An OrgChart carries the mode, the version shown (number, effectiveFrom, state: current, scheduled or past, note), the versions list and the seats. With one chart for the organization it also lists teamSeatIds -- the seats of the requested team's part. An unknown version is 404.

A seat is { id, parentId, title, responsibilities, people, teams }:

  • id stays the same across versions (keep it when you move or rename a seat; new seats need a new id of 1--40 letters, digits, _ or -); siblings keep the order of the list.
  • people: up to 10 of { userId, name, note, acting } -- userId of an organization member (the name is taken from the account) or null for an external person; acting marks someone who holds the seat for the time being.
  • responsibilities: up to 12 texts; teams: { teamId, scope } with scope branch (the seat and everything below it) or seat (only this seat) -- only in a company chart.

A save sends the revision it is based on; if someone saved the draft in the meantime, the API answers 409 and you load the draft again. A chart with a cycle, an unknown parent, more than 500 seats or a person outside the organization is 422. Publishing on a day before the latest version's is 422.

People Analyzer (people-reviews) ​

PUT /teams/{teamId}/people-reviews creates or replaces your review of one team member for one quarter and returns it (200 PeopleReview). You are always the evaluator (evaluatorId). There is one review per member, evaluator, and quarter -- sending it again for the same member and quarter replaces the previous one.

FieldTypeRequiredNotes
userIdstringyesThe reviewed person, a member of the team
quarterstringyesYYYY-Qn, e.g. "2026-Q4" (years 2000--2100)
coreValuesobjectnoCore value → rating "+", "+/-" or "-", e.g. {"Customer first": "+", "Ownership": "+/-"}; leave a value out while it isn't rated. Up to 50 values, names up to 200 characters
coreValuesScoresobjectnoDeprecated: core value → true (+) or false (-); a false keeps a stored +/- (so reading and writing the booleans back changes nothing). Ignored when coreValues is sent
gwcobjectyes{"getsIt": true, "wantsIt": true, "capacityToDoIt": false} -- yes (true), no (false), or null / left out while not answered
notesstringnoUp to 5,000 characters

PeopleReview has coreValues (the ratings), coreValuesScores (for older clients: true for +, false for +/- and -), gwc with null for unanswered, notes, userId, evaluatorId, quarter. A rating of a core value that is no longer in the company's list stays in the review but no longer counts.

Who reads which reviews. GET /teams/{teamId}/people-reviews returns only your own reviews, unless you are an Admin or Owner of the team (or an organization Owner, Admin or Implementer): then everyone's. Before v1.22 every team member read everyone's.

Documents ​

EndpointPurposeResponse
POST /teams/{teamId}/documentsAdd a link to the team's documents201 Document
DELETE /documents/{id}Delete a document or link204
FieldTypeRequiredNotes
namestringyes1--200 characters
urlstringyesAn http:// or https:// address, up to 2,000 characters

The API adds links (type: "LINK"); it does not upload files.

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.

Record a past meeting and rate it. Monday's L10 started at 9:00 Bratislava time and took 90 minutes (recording needs a team Admin):

bash
curl -s -X POST "$BASE/teams/$TEAM_ID/meetings" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"startedAt": "2026-10-05T09:00:00+02:00", "durationMin": 90}'
json
{
  "id": "cmg6n…",
  "teamId": "cmg2b…",
  "date": "2026-10-05",
  "status": "COMPLETED",
  "startedAt": "2026-10-05T07:00:00.000Z",
  "endedAt": "2026-10-05T08:30:00.000Z",
  "durationMin": 90,
  "createdById": "cmg4x…",
  "rating": { "average": null, "ratedCount": 0, "absentCount": 0 }
}

Rate it yourself, and mark a colleague who was not there as absent:

bash
curl -s -X PUT "$BASE/meetings/cmg6n…/ratings/me" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rating": 9}'

curl -s -X PUT "$BASE/meetings/cmg6n…/ratings/cmg4y…" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"absent": true}'
json
{ "userId": "cmg4x…", "rating": 9, "absent": false }
{ "userId": "cmg4y…", "rating": 0, "absent": true }

The other members have 7 days from now to rate the meeting with their own keys -- unless the team's next meeting starts sooner.

Add a cascading message and acknowledge it. A Member of the team adds the message to the meeting:

bash
curl -s -X POST "$BASE/meetings/cmg6n…/messages" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "From November 1, travel expenses are approved in the new portal."}'
json
{
  "id": "cmgam…",
  "meetingId": "cmg6n…",
  "userId": "cmg4x…",
  "content": "From November 1, travel expenses are approved in the new portal.",
  "createdAt": "2026-10-07T10:02:17.000Z"
}

Anyone in the organization -- for example a colleague from another team, with their own key -- marks it as read:

bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$BASE/messages/acknowledge" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["cmgam…"]}'

The command prints 204. Acknowledging the same message again changes nothing.

Importing History ​

Bringing in a team's past -- old Scorecard weeks, past meetings with their ratings, headlines, to-dos and issues -- works with the endpoints above plus four helpers. They are made for scripts and AI assistants that import step by step and may have to repeat a step.

Repeatable creates: externalId. Every create of a to-do, issue, Rock, milestone, metric, meeting, segue, headline, and cascading message accepts externalId (1--200 characters): your own id for the item, unique per organization and kind of item. Sending the same externalId again creates nothing and returns the item created the first time with 200 instead of 201 (the body is the item as it is now; other fields of the repeated request are ignored). If that item was deleted in the meantime, it is created again. An externalId already used by an item of another team (or, for meeting items, another meeting) returns 409 conflict.

Undoable imports: importBatchId. Creates, metric values (PUT and batch), and member ratings accept importBatchId (1--100 characters). Everything sent with the same batch id can be removed at once:

EndpointPurposeResponse
DELETE /orgs/{orgId}/import-batches/{batchId}Delete what the batch created or set200 counts by type
DELETE /orgs/{orgId}/import-batches/{batchId}?dryRun=trueOnly count200 counts by type
json
{ "batchId": "sw-history-1", "dryRun": false,
  "deleted": { "metricValue": 412, "rating": 98, "headline": 31, "segue": 0, "message": 12, "task": 57, "issue": 40, "milestone": 0, "goal": 9, "meeting": 14, "metric": 10, "goalReport": 120 } }

It deletes metric values and ratings the batch set even where they replaced earlier ones (the earlier value is not restored), and frees the batch's externalIds. Deleting a meeting also deletes its guests, ratings, segues, headlines and messages. Organization Owners and Admins only.

Dry run. Any change can be tried first: send the header X-Dry-Run: true. The request is validated, authorized and executed in a database transaction that is then rolled back; the response is what would have been saved (a created item's id does not exist afterwards), and carries X-Dry-Run: true too. Errors come back exactly as they would without it.

History needs team Admin rights. Dates in the past (createdAt, completedAt, resolvedAt), another author (userId on segues, headlines and messages, createdById on issues) and other members' ratings are for team Admins and Owners; others get 403 forbidden with reason BACKFILL_FORBIDDEN. Dates may not lie in the future.

A typical order for one past meeting:

  1. POST /teams/{teamId}/meetings with startedAt, durationMin, externalId, importBatchId → the meeting (COMPLETED).
  2. PUT /meetings/{id}/guests for people outside the team.
  3. PUT /meetings/{id}/ratings/{userId} per member: { "rating": 8, "comment": "…" } or { "absent": true }.
  4. POST /meetings/{id}/segues, /headlines and /messages with userId and externalId.
  5. POST /teams/{teamId}/tasks and /issues with meetingId, the backfilled dates and externalId.
  6. PATCH /meetings/{id} with { "notes": "…" }.

Scorecard history goes week by week with POST /metrics/{id}/values/batch. A Rock's weekly statuses go with PUT /goals/{id}/reports/{week} (with importBatchId): a spreadsheet's "?" is UNKNOWN, a "-" is simply no report. A metric without a goal ("goal": null) records values that can't be judged, such as "20+".

Organization Administration ​

The endpoints of Organization settings in the web app: the organization's name, time zone, and language, its members, its teams, and the members of each team (see Organization Settings).

Who may. Organization Owners and Admins -- not organization Members (not even if they are Owner or Admin of a team) and not Implementers; they get 403 forbidden. Changes need a key with the write scope; in a suspended organization they return 409 orgSuspended. The member list GET /orgs/{orgId}/members is for Owners and Admins too (with the read scope).

EndpointPurposeResponse
PATCH /orgs/{orgId}Change the organization's name, time zone, or default language200 Organization
POST /orgs/{orgId}/membersAdd a member -- an existing account, or a new one that signs in with Google201 member with created
PATCH /orgs/{orgId}/members/{userId}Change a member's organization role200 OrganizationMember
DELETE /orgs/{orgId}/members/{userId}Remove a member from the organization and all its teams204
POST /orgs/{orgId}/teamsCreate a team201 Team
PATCH /teams/{teamId}Rename a team, set the leadership flag, change the meeting settings200 Team
DELETE /teams/{teamId}Delete a team with all its data204
POST /teams/{teamId}/membersAdd an organization member to the team201 TeamMember
PATCH /teams/{teamId}/members/{userId}Change a member's team role200 TeamMember
DELETE /teams/{teamId}/members/{userId}Remove a member from the team204

The responses have the same shape as GET /orgs/{orgId}, GET /orgs/{orgId}/members, GET /teams/{teamId}, and GET /teams/{teamId}/members.

Organization Settings ​

PATCH /orgs/{orgId} -- all fields optional, at least one required:

FieldTypeNotes
namestring1--200 characters
timezonestringAn IANA time zone, e.g. "Europe/Bratislava"; an unknown zone returns 422 invalid
defaultLocaleen | skThe language of new accounts created in this organization

Organization Members ​

POST /orgs/{orgId}/members adds a person by email:

FieldTypeRequiredNotes
emailstringyesThe person's email address
roleOWNER | ADMIN | MEMBER | IMPLEMENTERyesTheir organization role
namestringnoUp to 200 characters; used only when a new account is created
googleOnlybooleanfor a new accountMust be true when the email has no account yet
  • An existing account with this email is added to the organization. Its name and sign-in stay as they are.
  • An unknown email creates a new account that signs in with Google using that email -- send "googleOnly": true, otherwise the API answers 422 invalid. The account starts in the organization's default language.
  • No passwords. The API never sets a password, like Organization settings in the app. The body is strict: any other field, password included, returns 422 invalid.
  • The response is the member plus "created": true when a new account was created, false when an existing one was added. A person who already is a member returns 422 invalid.

PATCH /orgs/{orgId}/members/{userId} with { "role": "ADMIN" } changes the role; DELETE /orgs/{orgId}/members/{userId} removes the person from the organization and from all of its teams. Their history -- to-dos, issues, ratings -- stays, and their API keys lose access to the organization at once. A userId that is not a member of the organization returns 404 notFound.

Rules, as in the app:

  • Only Owners touch Owners and Admins. Granting the OWNER or ADMIN role, and changing or removing a member who is an Owner or Admin, needs an organization Owner; an Admin gets 403 forbidden.
  • The last Owner stays. Demoting or removing the organization's last Owner returns 422 invalid.

Managing Teams ​

POST /orgs/{orgId}/teams with { "name": "Sales" } (1--100 characters) creates a team. The slug is generated from the name ("Sales" → sales, or sales-2 if that slug is taken). A new team is not a leadership team, has the default meeting settings (90 minutes, warning at minute 80), and has no members yet.

PATCH /teams/{teamId} -- all fields optional, at least one required:

FieldTypeNotes
namestring1--100 characters. The slug does not change
isLeadershipbooleanLeadership teams manage company Rocks
meetingDurationMinintegerMeeting length in minutes, kept between 10 and 300
meetingWarningMinintegerThe minute from which the countdown warns, kept between 1 and one minute below the meeting length

Meeting settings are adjusted like in the app instead of being refused: a length of 400 is saved as 300, and a warning minute at or above the length is moved just below it -- {"meetingDurationMin": 60} on a team that warns at minute 80 saves a warning at minute 59. The response shows the saved values.

DELETE /teams/{teamId} deletes the team with all its data -- to-dos, issues, Rocks, the Scorecard, meetings, the team's V/TO traction, and everything else (the organization's company vision stays). It cannot be undone.

An unknown teamId returns 404 notFound; a team of an organization where you are not an Owner or Admin returns 403 forbidden.

Team Members ​

EndpointBodyNotes
POST /teams/{teamId}/membersuserId, role (optional, default MEMBER)The person must be a member of the team's organization (422 invalid otherwise -- add them with POST /orgs/{orgId}/members first). Someone already on the team returns 422 invalid
PATCH /teams/{teamId}/members/{userId}role404 notFound if the person is not on the team
DELETE /teams/{teamId}/members/{userId}--Removes the person from this team only (404 notFound if they are not on it)

Team roles are OWNER, ADMIN, MEMBER, and VIEWER (see Roles & Permissions).

Example: Add a Team Member ​

Add an existing colleague to the organization, then to the Sales team as a team Admin:

bash
curl -s -X POST "$BASE/orgs/$ORG_ID/members" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "peter@acme.com", "role": "MEMBER"}'
json
{ "userId": "cmg4z…", "email": "peter@acme.com", "name": "Peter Kováč", "role": "MEMBER", "joinedAt": "2026-10-07T11:20:05.000Z", "created": false }
bash
curl -s -X POST "$BASE/teams/$TEAM_ID/members" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"userId": "cmg4z…", "role": "ADMIN"}'
json
{ "userId": "cmg4z…", "email": "peter@acme.com", "name": "Peter Kováč", "role": "ADMIN" }

For someone without an account, add "googleOnly": true (and optionally a name) to the first request -- the response then has "created": true, and the person signs in with Google using that email.

Platform Administration ​

The endpoints of Platform in the web app (see Platform Administration), for the platform operator.

Who may. Only a key with the platform scope works here, and only while its owner is a platform operator (SUPERADMIN). Only platform operators can create such keys, valid for at most 90 days (see Platform Keys). Any other key gets 403.

No organization data

Platform endpoints manage tenants, accounts, and AI settings. They never return organization data -- no teams, to-dos, Rocks, meetings, or members of an organization, only names and counts. Like in the web app, the platform operator sees an organization's data only as its member, with a read or write key and their organization role.

EndpointPurposeResponse
GET /platform/orgsAll organizations200 list of PlatformOrganization
POST /platform/orgsCreate an organization with its first Owner201 PlatformOrganization
PATCH /platform/orgs/{id}Suspend or resume an organization200 PlatformOrganization
GET /platform/usersAll accounts with counts (paginated)200 list of PlatformUser
PATCH /platform/users/{id}Make a user a platform admin or a plain user200 PlatformUser
GET /platform/users/{id}/api-keysOne account's API keys (details only)200 list of ApiKeyInfo
DELETE /platform/api-keys/{id}Revoke any user's API key204
GET /platform/aiAI providers (keys masked) and system prompts200 AiConfig
PUT /platform/ai/providers/{type}Set a provider's API key and default model200 AiProvider
PATCH /platform/ai/providers/{type}Switch a provider on or off200 AiProvider
DELETE /platform/ai/providers/{type}Delete a provider204, or 404 if there is none
PUT /platform/ai/prompts/{inputType}Set the system prompt for an input type200 AiPrompt
DELETE /platform/ai/prompts/{inputType}Delete a system prompt -- the default applies again204, or 404 if there is none

Organizations ​

A PlatformOrganization has id, name, slug, status (ACTIVE or SUSPENDED), members and teams (counts), and createdAt.

POST /platform/orgs creates an organization and makes its first Owner:

FieldTypeRequiredNotes
namestringyes1--200 characters; the slug is generated from it
timezonestringyesAn IANA time zone, e.g. "Europe/Bratislava"
defaultLocaleen | skyes
ownerobjectyesemail, and for a new account name (optional) plus either password (8--200 characters) or "googleOnly": true

An existing account with the owner's email becomes the Owner as it is; an unknown email without a password or googleOnly returns 422 invalid.

PATCH /platform/orgs/{id} with { "status": "SUSPENDED" } makes the organization read only -- its members can still read, and every change returns 409 orgSuspended, in the app and in the API. { "status": "ACTIVE" } resumes it.

Users and Their API Keys ​

GET /platform/users lists every account, oldest first: id, email, name, systemRole (SUPERADMIN or USER), orgCount, activeApiKeys, and createdAt -- counts only, not which organizations.

PATCH /platform/users/{id} with { "systemRole": "SUPERADMIN" } makes the user a platform admin, { "systemRole": "USER" } a plain user again. It applies at once: a removed admin's keys and AI connections keep working but lose the platform scope. Your own role cannot be changed (409 conflict), so at least one platform admin always remains.

GET /platform/users/{id}/api-keys lists one account's keys with id, name, prefix (the visible beginning, e.g. ak_k3m7q2xa), scopes, status (active, expired, revoked), expiresAt, lastUsedAt, and createdAt. The key itself is never stored, so it is never returned.

DELETE /platform/api-keys/{id} revokes a key -- for example one that may have leaked. Integrations using it stop working at once. Revoking a key that is already revoked changes nothing. Keys cannot be created on anyone's behalf.

AI ​

GET /platform/ai returns providers -- id, type, defaultModel, isActive, and apiKeyMasked -- and prompts -- id, inputType, systemPrompt, providerId, modelOverride, and isActive. Only custom prompts are listed; input types without one use the built-in default prompt.

EndpointBodyNotes
PUT /platform/ai/providers/{type}apiKey (8--500 characters), defaultModel (1--200 characters){type} is ANTHROPIC or OPENROUTER. Creates the provider or replaces its key and model. The key is stored encrypted and never returned -- responses show it masked. A new provider is active
PATCH /platform/ai/providers/{type}isActive (boolean)Switches the provider on or off
DELETE /platform/ai/providers/{type}--Prompts that used it fall back to the first active provider
PUT /platform/ai/prompts/{inputType}systemPrompt (1--20,000 characters), providerId (optional), modelOverride (optional, up to 200 characters)Creates or replaces the prompt. providerId is a provider id from GET /platform/ai (422 for an unknown one); omitted or null -- or a provider that is switched off -- uses the first active provider. modelOverride omitted, null, or "" uses the provider's default model
DELETE /platform/ai/prompts/{inputType}--The input type uses its default prompt again

The input types are TODO_TITLE, ISSUE_TITLE, ISSUE_DESCRIPTION, ROCK_TITLE, ROCK_DESCRIPTION, MEASURABLE_TITLE, HEADLINE, CASCADING_MESSAGE, and VTO_CONTENT (see AI Integration).

Example: Suspend an Organization ​

With a platform key in EOSHUB_KEY, find the organization and suspend it:

bash
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/platform/orgs" | jq -r '.data[] | "\(.id)  \(.status)  \(.name)"'

curl -s -X PATCH "$BASE/platform/orgs/cmg1a…" \
  -H "Authorization: Bearer $EOSHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "SUSPENDED"}'
json
{ "id": "cmg1a…", "name": "Acme Corp", "slug": "acme-corp", "status": "SUSPENDED", "members": 5, "teams": 2, "createdAt": "2026-05-04T08:00:00.000Z" }

The organization's members can still read, but every change now returns 409 orgSuspended. Send {"status": "ACTIVE"} to resume it.

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.