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 URL | https://<your-host>/api/v1 |
| Interactive reference | https://<your-host>/api/v1/docs -- browse every endpoint and try calls with your own key |
| OpenAPI spec | https://<your-host>/api/v1/openapi.json (OpenAPI 3.1) -- for code generators and API clients |
| Authentication | Authorization: Bearer ak_… (personal API key) |
| Format | JSON, 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:
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:
| Scope | Allows | Who can create it |
|---|---|---|
read | Reading everything your roles allow | Everyone (Read only in the create dialog) |
write | Creating and changing data as your roles allow; implies read | Everyone (Read & write) |
platform | Platform administration | Platform 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:
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:
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/me"{
"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:
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/orgs/cmg1a…/teams"{
"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:
curl -s -H "Authorization: Bearer $EOSHUB_KEY" "$BASE/teams/cmg2b…/tasks?completed=false"{
"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:
{ "data": [ … ], "nextCursor": "eyJrIjoiMjAyNi0xMC0wNlQwODo0MTowMi4wMDBaIiwiaWQiOiJjbWc3dCJ9" }| Parameter | Description |
|---|---|
limit | Page size, 1--200 (default 50) |
cursor | The 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:
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
doneconst 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:
{
"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"
}
]
}
}| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthenticated | Missing, malformed, expired, or revoked API key |
| 403 | forbidden | Your roles do not allow access to this organization or team, or do not allow this change |
| 403 | insufficientScope | The key's scopes do not allow this operation |
| 404 | notFound | The item does not exist, or you cannot see it |
| 409 | orgSuspended | The organization is suspended (read only) -- every change is refused |
| 409 | conflict | An externalId already names an item of another team or meeting, or two requests import the same externalId at once (see Importing History) |
| 409 | ratingClosed | The 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 |
| 422 | invalid | Invalid parameters or body; for format errors issues lists the problems field by field (see Validation) |
| 429 | rateLimited | Too many requests -- see Rate Limit |
| 500 | internal | Unexpected server error (no details are returned) |
Reasons. Where the code alone doesn't say which check failed, error.details.reason does:
{ "error": { "code": "invalid", "message": "A scorecard week is its Monday (YYYY-MM-DD)", "details": { "reason": "INVALID_WEEK" } } }reason | code | Meaning |
|---|---|---|
INVALID_WEEK | invalid | A Scorecard week that is not a Monday |
OWNER_NOT_MEMBER | invalid | The owner (or author) is not a member of the team |
INVALID_RATING | invalid | A meeting rating that is not a whole number from 1 to 10 |
NOT_TEAM_MEETING | invalid | The meetingId is a meeting of another team |
BACKFILL_FORBIDDEN | forbidden | Writing 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
| Kind | Format | Example |
|---|---|---|
| Field names | camelCase | dueDate, isLeadership |
| Ids | Opaque 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" |
| Quarters | YYYY-Qn | "2026-Q3" |
| Enum values | UPPER_CASE | OPEN, ON_TRACK, COMPLETED |
| Missing values | null (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"+","+/-","-") andgwc(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 Hub | In the API |
|---|---|
| Organization, team | orgs, teams |
| To-Do | tasks |
| Issue | issues |
| Rock, milestone | goals (milestones are embedded in each goal) |
| Scorecard measurable, weekly value | metrics, metrics/{id}/values |
| L10 Meeting | meetings (with ratings, segues, headlines, messages) |
| V/TO | vision |
| Accountability Chart | org-chart (drafts and versions) |
| People Analyzer | people-reviews |
| Documents | documents |
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
| Endpoint | Returns | Filters |
|---|---|---|
/me | Your account, organizations with your role, and the key's scopes | -- |
/orgs | Your organizations, with your role in each | -- |
/orgs/{orgId} | One organization | -- |
/orgs/{orgId}/members | All 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}/teams | Teams of the organization you can see | -- |
/orgs/{orgId}/vision | The company vision, shared by all teams of the organization (see V/TO) | -- |
Teams
| Endpoint | Returns | Filters |
|---|---|---|
/teams/{teamId} | One team, including its meeting duration settings | -- |
/teams/{teamId}/members | Team members with their team role (paginated) | -- |
Team Data
| Endpoint | Returns | Filters |
|---|---|---|
/teams/{teamId}/tasks | To-Dos (paginated) | completed (true/false), ownerId, meetingId |
/tasks/{id} | One to-do | -- |
/teams/{teamId}/issues | Issues (paginated) | status (OPEN, SOLVING, SOLVED), priority (HIGH, MEDIUM, LOW), longTerm (true/false), meetingId |
/issues/{id} | One issue | -- |
/teams/{teamId}/goals | Rocks with their milestones (paginated) | quarter (2026-Q3), status (ON_TRACK, OFF_TRACK, DONE), level (COMPANY, DEPARTMENT) |
/orgs/{orgId}/goals | Company 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}/metrics | Scorecard measurables | -- |
/metrics/{id}/values | Weekly values of a measurable, oldest first (paginated) | from, to (days, inclusive) |
/teams/{teamId}/meetings | Meetings, 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}/vision | The 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-chart | The Accountability Chart that applies to the team; ?version=n for a published version | -- |
/teams/{teamId}/people-reviews | People Analyzer entries: your own, or everyone's for the team's Admins (paginated) | quarter (2026-Q3) |
/teams/{teamId}/documents | Documents 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.
| Endpoint | Returns | Filters |
|---|---|---|
/platform/orgs | All organizations with their status and the number of members and teams | -- |
/platform/users | All accounts with the number of organizations and active API keys, oldest first (paginated) | -- |
/platform/users/{id}/api-keys | One account's API keys, newest first -- details only, never the key itself | -- |
/platform/ai | AI providers (API keys masked) and the custom system prompts | -- |
/platform/feedback | Feedback 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
| Method | Use | Success response |
|---|---|---|
POST | Create an item | 201 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) |
PATCH | Partial update -- send only the fields you want to change; omitted fields keep their values | 200 OK with the updated item |
PUT | Create or replace: a metric's value for one week, a meeting rating, your People Analyzer review -- idempotent, sending the same request again changes nothing | 200 OK with the saved item |
DELETE | Delete an item | 204 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.
| Operation | Allowed 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-term | Team Members and above |
| Enter weekly metric values | Team Members and above |
| Change a Rock's status; add, edit, complete, and delete its milestones | The Rock's owner (Member or above) and team Admins |
| Create Rocks; change their title, description, owner, quarter, or level | Team Admins and Owners |
| Add, edit, and reorder metrics | Team Admins and Owners |
| Schedule and record (backfill) meetings, start and end them, correct a completed meeting's times | Team Admins and Owners |
| Rate a meeting yourself | Team 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 absent | Team Admins and Owners |
| Add segues, headlines, and cascading messages | Team Members and above |
| Acknowledge (mark as read) cascading messages | Any 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 links | Team Members and above |
| Delete to-dos, issues, Rocks, metrics, cascading messages, and documents | Team 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 guests | Team 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
ownerIdyou send for a to-do, Rock, or metric must be a member of the item's team (the ids are inGET /teams/{teamId}/members); otherwise the API answers422 invalidwith reasonOWNER_NOT_MEMBER. Other edits work even if the current owner has left the team. - One request, one change. A
PATCHwith 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 (
413otherwise). - Meetings must belong to the team. The optional
meetingIdof a new to-do or issue must be a meeting of the same team (reasonNOT_TEAM_MEETINGotherwise). - Suspended organizations are read only. Every change in a suspended organization returns
409 orgSuspended. - Calendar days (
dueDate, the metricweek, the meetingdate) accept onlyYYYY-MM-DD. An instant such as2026-10-14T00:00:00Zor a non-existent day such as2026-02-30is refused with422 invalid. - Instants (the meeting
startedAt, the backfilledcreatedAt,completedAt,resolvedAt) need ISO 8601 with a time zone:2026-10-05T09:00:00+02:00or2026-10-05T07:00:00Z. A time without a zone, such as2026-10-05T09:00:00, is refused with422 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
PATCHbody. The response lists each problem inissueswith the field'spath. - 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
invalidbut noissues.
To-Dos (tasks)
| Endpoint | Purpose | Response |
|---|---|---|
POST /teams/{teamId}/tasks | Create a to-do | 201 Task |
PATCH /tasks/{id} | Change, complete, or reopen a to-do | 200 Task |
DELETE /tasks/{id} | Delete a to-do | 204 |
Create body:
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | 1--500 characters |
ownerId | string | yes | A member of the team |
dueDate | day | yes | YYYY-MM-DD |
meetingId | string | no | The meeting of the same team the to-do came from |
completed | boolean | no | true creates it done (completedAt now unless given) |
completedAt | instant | no | When it was done -- needs completed: true; not in the future. Team Admins only |
createdAt | instant | no | When it was created; not in the future. Team Admins only |
externalId, importBatchId | string | no | See Importing History |
GET /teams/{teamId}/tasks?meetingId=… lists the to-dos created in one meeting.
Update body -- all fields optional, at least one required:
| Field | Type | Notes |
|---|---|---|
title | string | 1--500 characters |
ownerId | string | A member of the team |
dueDate | day | YYYY-MM-DD |
completed | boolean | true completes the to-do and sets completedAt; false reopens it and clears completedAt |
Issues
| Endpoint | Purpose | Response |
|---|---|---|
POST /teams/{teamId}/issues | Create an issue | 201 Issue |
PATCH /issues/{id} | Change an issue or its status | 200 Issue |
DELETE /issues/{id} | Delete an issue | 204 |
Create body:
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | 1--500 characters |
description | string | no | Up to 5,000 characters |
priority | HIGH | MEDIUM | LOW | no | Default MEDIUM |
longTerm | boolean | no | true 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 |
meetingId | string | no | The meeting of the same team the issue came from |
status | OPEN | SOLVING | SOLVED | no | Default OPEN; SOLVED sets resolvedAt (now unless given) |
createdById | string | no | Who raised it, when not you -- a member of the team. Team Admins only |
createdAt | instant | no | When it was raised; not in the future. Team Admins only |
resolvedAt | instant | no | When it was solved -- needs status: SOLVED; not in the future. Team Admins only |
externalId, importBatchId | string | no | See 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:
| Field | Type | Notes |
|---|---|---|
title | string | 1--500 characters |
description | string | Up to 5,000 characters; "" clears it |
priority | HIGH | MEDIUM | LOW | |
status | OPEN | SOLVING | SOLVED | SOLVED sets resolvedAt to now; OPEN and SOLVING clear it |
longTerm | boolean | Moves the issue to the long-term (true) or short-term (false) list |
Rocks (goals) and Milestones
| Endpoint | Purpose | Response |
|---|---|---|
POST /teams/{teamId}/goals | Create a Rock | 201 Goal |
PATCH /goals/{id} | Change a Rock or report its status | 200 Goal |
DELETE /goals/{id} | Delete a Rock with its milestones | 204 |
GET /goals/{id}/reports | The Rock's weekly reports, oldest first | 200 list of GoalReport |
PUT /goals/{id}/reports/{week} | Report a week (its Monday): status (ON_TRACK, OFF_TRACK, DONE, UNKNOWN), note | 200 GoalReport |
DELETE /goals/{id}/reports/{week} | Remove a week's report | 204 |
POST /goals/{id}/milestones | Add a milestone to a Rock | 201 Milestone |
PATCH /milestones/{id} | Change, complete, or reopen a milestone | 200 Milestone |
DELETE /milestones/{id} | Delete a milestone | 204 |
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:
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | 1--300 characters |
description | string | no | Up to 5,000 characters |
ownerId | string | yes | A member of the team |
quarter | string | yes | YYYY-Qn, e.g. "2026-Q4" (years 2000--2100) |
level | COMPANY | DEPARTMENT | no | Default 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:
| Field | Type | Notes |
|---|---|---|
title | string | 1--300 characters |
description | string | Up to 5,000 characters; "" clears it |
ownerId | string | A member of the team |
quarter | string | YYYY-Qn |
level | COMPANY | DEPARTMENT | COMPANY only in a leadership team |
status | ON_TRACK | OFF_TRACK | DONE | The only field the Rock's owner may change; all other fields need a team Admin |
Milestone bodies:
| Field | Type | Create | Update | Notes |
|---|---|---|---|---|
title | string | required | optional | 1--300 characters |
dueDate | day or null | optional, default null | optional | YYYY-MM-DD; null removes the due date |
completed | boolean | -- | optional | true completes, false reopens. New milestones start not completed |
Scorecard Metrics (metrics)
| Endpoint | Purpose | Response |
|---|---|---|
POST /teams/{teamId}/metrics | Add a metric at the end of the team's Scorecard | 201 Metric |
PATCH /metrics/{id} | Change or reorder a metric | 200 Metric |
DELETE /metrics/{id} | Delete a metric with all its values | 204 |
PUT /metrics/{id}/values/{week} | Set the value of one week | 200 MetricValue |
POST /metrics/{id}/values/batch | Set the values of many weeks (up to 500, body at most 64 KB), each saved on its own | 200 per-week results |
DELETE /metrics/{id}/values/{week} | Clear the value of one week | 204 (404 if the week has none) |
Create body:
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | 1--200 characters |
goal | string | null | no | Up 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) |
ownerId | string | yes | A member of the team |
unit | string | no | Up to 20 characters, e.g. "€" or "%" |
description | string | no | Up to 5,000 characters |
externalId, importBatchId | string | no | See Importing History |
Update body -- all fields optional, at least one required:
| Field | Type | Notes |
|---|---|---|
title, ownerId | string | As when creating |
goal | string | null | As when creating; null makes the metric tracking-only |
unit, description | string | As 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, asYYYY-MM-DD. Any other day returns422 invalidwith reasonINVALID_WEEK.- The request is an upsert: it creates the week's value or replaces it. Repeating it gives the same result.
enteredByIdkeeps 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).
| Field | Type | Required | Notes |
|---|---|---|---|
value | string | yes | Up to 50 characters. Send numbers as strings: "12", not 12 |
onTrack | boolean | no | Leave 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 |
note | string | no | Up to 500 characters: a note on the week's value. Replaces the stored note; "" clears it; left out keeps it |
importBatchId | string | no | See Importing History |
Many weeks at once -- POST /metrics/{id}/values/batch:
{
"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:
{ "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
| Endpoint | Purpose | Response |
|---|---|---|
POST /teams/{teamId}/meetings | Schedule a meeting, or record one that already happened | 201 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-ratings | Reveal a running meeting's ratings (facilitators) | 204 |
GET /meetings/{id}/recap | Recap 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/me | Rate the meeting yourself | 200 Rating |
PUT /meetings/{id}/ratings/{userId} | Set or clear a member's rating (with a comment), or mark them absent | 200 Rating, or 204 when cleared |
PUT /meetings/{id}/guests | Replace who else attended (people outside the team) | 200 { data: MeetingGuest[] } |
POST /meetings/{id}/segues | Add a segue (good news) | 201 Segue |
POST /meetings/{id}/headlines | Add a customer, employee or company headline; userId (whose it is): any team member in a running meeting, otherwise team Admins | 201 Headline |
POST /meetings/{id}/messages | Add a cascading message | 201 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 segue | 204 |
PATCH /headlines/{id} | Correct a headline: content, type, userId (whose it is) | 200 Headline |
DELETE /headlines/{id} | Delete a headline | 204 |
PATCH /messages/{id} | Correct a cascading message: { "content" } | 200 CascadingMessage |
DELETE /messages/{id} | Delete a cascading message | 204 |
POST /messages/acknowledge | Mark cascading messages as read | 204 |
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:
| Shape | Field | Type | Notes |
|---|---|---|---|
| Schedule | date | day | The day of the meeting, YYYY-MM-DD. The meeting starts as SCHEDULED |
| Record a past meeting | startedAt | instant | When the meeting started, with a time zone offset. Not in the future |
durationMin | integer | Length in whole minutes, 1--600 | |
| Either shape | externalId, importBatchId | string | Optional; 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:
| Shape | Field | Type | Notes |
|---|---|---|---|
| Advance the status | status | IN_PROGRESS | COMPLETED | Only 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 times | startedAt | instant | Completed meetings only (422 otherwise). Not in the future |
durationMin | integer | Whole 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/mewith{ "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 forbiddenotherwise).- 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 get409 ratingClosed. Team Admins may rate at any time.GET /meetings/{id}shows the window asratingWindow.openandratingWindow.closesAt. PUT /meetings/{id}/ratings/{userId}is for team Admins (and Implementers) and is not limited by the window, only bySCHEDULED. TheuserIdmust be a member of the team (422 invalidotherwise). A rating outside 1--10 returns422 invalidwith reasonINVALID_RATING. Each shape below may also carrycomment(up to 500 characters: why this number;""clears it) andimportBatchId.
The body must be exactly one of these shapes:
| Body | Effect | Response |
|---|---|---|
{ "rating": 8 } | Sets the member's rating (1--10) and marks them present | 200 Rating |
{ "rating": 0 } | Clears the member's rating: they count as present but not rated | 204 No Content |
{ "absent": true } | Marks the member absent and stores the rating 0. Absent members do not count toward the average | 200 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:
| Endpoint | Field | Type | Notes |
|---|---|---|---|
segues, messages | content | string | 1--1,000 characters |
headlines | type | CUSTOMER | EMPLOYEE | COMPANY | COMPANY: company news |
content | string | 1--1,000 characters | |
| all three | userId | string | Optional: who said it, when not you -- a member of the team. Team Admins only |
| all three | externalId, importBatchId | string | Optional; 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.
| Endpoint | Purpose | Response |
|---|---|---|
GET /orgs/{orgId}/vision | The company vision | 200 CompanyVision |
PATCH /orgs/{orgId}/vision | Set one field of the company vision | 200 CompanyVision |
GET /teams/{teamId}/vision | The team's V/TO: company vision and team traction | 200 Vision |
PATCH /teams/{teamId}/vision | Set the team's 1-year plan, or clear the archive | 200 Vision |
POST /teams/{teamId}/vision | Create 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:
| Field | Type | Notes |
|---|---|---|
orgId | string | |
coreValues | list of texts | |
coreFocus | object | Texts, e.g. purpose, niche |
tenYearTarget | text | |
marketingStrategy | object | Texts or lists of texts, e.g. targetMarket, threeUniques, provenProcess, guarantee |
threeYearPicture | text or object | An object of texts or lists of texts, e.g. revenue, profit, measurables |
peopleBar | number | People Analyzer: how many +/- a right person may have on the core values (a - never passes); default 1 |
updatedAt | instant or null | null 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:
| Field | Type | Notes |
|---|---|---|
teamId | string | |
companyVision | CompanyVision | The organization's vision, the same for every team |
oneYearPlan | text or object | An object of texts or lists of texts, e.g. revenue, profit, goals |
quarter | quarter | The current quarter in your time zone, e.g. "2026-Q4" |
goals | list of Goal | The 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 |
longTermIssues | list of Issue | The team's open (OPEN, SOLVING) long-term issues, live, in the same shape as GET /issues/{id} |
archive | object | quarterlyRocks 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 |
updatedAt | instant or null | When 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:
| Field | Type | Notes |
|---|---|---|
field | string | One of coreValues, coreFocus, tenYearTarget, marketingStrategy, threeYearPicture, peopleBar |
value | depends on field | See the next table. Up to 20,000 characters as stored |
renames | object | Optional, 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:
| Fields | value |
|---|---|
coreValues | A list of texts: at most 100 items, each 1--1,000 characters (trimmed) |
coreFocus, marketingStrategy | An 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, oneYearPlan | Text, stored as sent (not trimmed), or an object as above |
tenYearTarget | Text, stored as sent (not trimmed) |
peopleBar | A whole number 0--10 |
quarterlyRocks, issuesList | Only "" 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:
{ "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):
{ "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.
| Endpoint | Purpose | Response |
|---|---|---|
GET /teams/{teamId}/org-chart | The chart that applies to the team. Optional ?version=n reads a published version instead of the current one | 200 OrgChart |
GET /orgs/{orgId}/org-chart | The organization's chart (only with one chart for the organization; 404 with a chart per team) | 200 OrgChart |
POST /teams/{teamId}/org-chart/draft | Open 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 one | 201 ChartDraft |
GET /teams/{teamId}/org-chart/draft | The open draft (404 when there is none) | 200 ChartDraft |
PUT /teams/{teamId}/org-chart/draft | Save the whole draft: {"revision": 3, "seats": [...]} | 200 { revision, updatedAt } |
DELETE /teams/{teamId}/org-chart/draft | Discard the draft | 204 |
POST /teams/{teamId}/org-chart/draft/publish | Publish it: {"effectiveFrom": "2026-11-01", "note": "…"}. A future day schedules the version | 200 OrgChart |
POST /teams/{teamId}/org-chart/versions/{number}/withdraw | Take 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 }:
idstays 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 }--userIdof an organization member (the name is taken from the account) ornullfor an external person;actingmarks someone who holds the seat for the time being.responsibilities: up to 12 texts;teams:{ teamId, scope }withscopebranch(the seat and everything below it) orseat(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.
| Field | Type | Required | Notes |
|---|---|---|---|
userId | string | yes | The reviewed person, a member of the team |
quarter | string | yes | YYYY-Qn, e.g. "2026-Q4" (years 2000--2100) |
coreValues | object | no | Core 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 |
coreValuesScores | object | no | Deprecated: core value → true (+) or false (-); a false keeps a stored +/- (so reading and writing the booleans back changes nothing). Ignored when coreValues is sent |
gwc | object | yes | {"getsIt": true, "wantsIt": true, "capacityToDoIt": false} -- yes (true), no (false), or null / left out while not answered |
notes | string | no | Up 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
| Endpoint | Purpose | Response |
|---|---|---|
POST /teams/{teamId}/documents | Add a link to the team's documents | 201 Document |
DELETE /documents/{id} | Delete a document or link | 204 |
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | 1--200 characters |
url | string | yes | An 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:
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"}'{
"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:
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:
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"}'{ "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:
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):
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}'{
"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:
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}'{ "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:
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."}'{
"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:
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:
| Endpoint | Purpose | Response |
|---|---|---|
DELETE /orgs/{orgId}/import-batches/{batchId} | Delete what the batch created or set | 200 counts by type |
DELETE /orgs/{orgId}/import-batches/{batchId}?dryRun=true | Only count | 200 counts by type |
{ "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:
POST /teams/{teamId}/meetingswithstartedAt,durationMin,externalId,importBatchId→ the meeting (COMPLETED).PUT /meetings/{id}/guestsfor people outside the team.PUT /meetings/{id}/ratings/{userId}per member:{ "rating": 8, "comment": "…" }or{ "absent": true }.POST /meetings/{id}/segues,/headlinesand/messageswithuserIdandexternalId.POST /teams/{teamId}/tasksand/issueswithmeetingId, the backfilled dates andexternalId.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).
| Endpoint | Purpose | Response |
|---|---|---|
PATCH /orgs/{orgId} | Change the organization's name, time zone, or default language | 200 Organization |
POST /orgs/{orgId}/members | Add a member -- an existing account, or a new one that signs in with Google | 201 member with created |
PATCH /orgs/{orgId}/members/{userId} | Change a member's organization role | 200 OrganizationMember |
DELETE /orgs/{orgId}/members/{userId} | Remove a member from the organization and all its teams | 204 |
POST /orgs/{orgId}/teams | Create a team | 201 Team |
PATCH /teams/{teamId} | Rename a team, set the leadership flag, change the meeting settings | 200 Team |
DELETE /teams/{teamId} | Delete a team with all its data | 204 |
POST /teams/{teamId}/members | Add an organization member to the team | 201 TeamMember |
PATCH /teams/{teamId}/members/{userId} | Change a member's team role | 200 TeamMember |
DELETE /teams/{teamId}/members/{userId} | Remove a member from the team | 204 |
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:
| Field | Type | Notes |
|---|---|---|
name | string | 1--200 characters |
timezone | string | An IANA time zone, e.g. "Europe/Bratislava"; an unknown zone returns 422 invalid |
defaultLocale | en | sk | The language of new accounts created in this organization |
Organization Members
POST /orgs/{orgId}/members adds a person by email:
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | The person's email address |
role | OWNER | ADMIN | MEMBER | IMPLEMENTER | yes | Their organization role |
name | string | no | Up to 200 characters; used only when a new account is created |
googleOnly | boolean | for a new account | Must 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 answers422 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,
passwordincluded, returns422 invalid. - The response is the member plus
"created": truewhen a new account was created,falsewhen an existing one was added. A person who already is a member returns422 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
OWNERorADMINrole, and changing or removing a member who is an Owner or Admin, needs an organization Owner; an Admin gets403 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:
| Field | Type | Notes |
|---|---|---|
name | string | 1--100 characters. The slug does not change |
isLeadership | boolean | Leadership teams manage company Rocks |
meetingDurationMin | integer | Meeting length in minutes, kept between 10 and 300 |
meetingWarningMin | integer | The 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
| Endpoint | Body | Notes |
|---|---|---|
POST /teams/{teamId}/members | userId, 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} | role | 404 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:
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"}'{ "userId": "cmg4z…", "email": "peter@acme.com", "name": "Peter Kováč", "role": "MEMBER", "joinedAt": "2026-10-07T11:20:05.000Z", "created": false }curl -s -X POST "$BASE/teams/$TEAM_ID/members" \
-H "Authorization: Bearer $EOSHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "cmg4z…", "role": "ADMIN"}'{ "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.
| Endpoint | Purpose | Response |
|---|---|---|
GET /platform/orgs | All organizations | 200 list of PlatformOrganization |
POST /platform/orgs | Create an organization with its first Owner | 201 PlatformOrganization |
PATCH /platform/orgs/{id} | Suspend or resume an organization | 200 PlatformOrganization |
GET /platform/users | All accounts with counts (paginated) | 200 list of PlatformUser |
PATCH /platform/users/{id} | Make a user a platform admin or a plain user | 200 PlatformUser |
GET /platform/users/{id}/api-keys | One account's API keys (details only) | 200 list of ApiKeyInfo |
DELETE /platform/api-keys/{id} | Revoke any user's API key | 204 |
GET /platform/ai | AI providers (keys masked) and system prompts | 200 AiConfig |
PUT /platform/ai/providers/{type} | Set a provider's API key and default model | 200 AiProvider |
PATCH /platform/ai/providers/{type} | Switch a provider on or off | 200 AiProvider |
DELETE /platform/ai/providers/{type} | Delete a provider | 204, or 404 if there is none |
PUT /platform/ai/prompts/{inputType} | Set the system prompt for an input type | 200 AiPrompt |
DELETE /platform/ai/prompts/{inputType} | Delete a system prompt -- the default applies again | 204, 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:
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | 1--200 characters; the slug is generated from it |
timezone | string | yes | An IANA time zone, e.g. "Europe/Bratislava" |
defaultLocale | en | sk | yes | |
owner | object | yes | email, 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.
| Endpoint | Body | Notes |
|---|---|---|
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:
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"}'{ "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.
Related Pages
- API Keys -- Creating, revoking, and securing keys
- Roles & Permissions -- What each role can see and do
- Organizations & Teams -- How organizations and teams work