Requests and errors
The API has one request shape for mutations and one for reads. Both are plain JSON over HTTPS against https://os.cohortapp.com/api/v1.
Call a method (POST)
Send a POST to /api/v1/{family}.{method} with the method's parameters under a params key:
curl -X POST https://os.cohortapp.com/api/v1/board.createTask \
-H "Authorization: Bearer $COHORT_API_KEY" \
-H "Content-Type: application/json" \
-H "x-idempotency-key: task-launch-brief-1" \
-d '{"params": {"title": "Draft the launch brief", "priority": "P1"}}'
A bare params object as the body (no params wrapper) is also accepted. Every POST response — success or failure — is a frame:
{ "ok": true, "result": { "id": "task_…", "title": "Draft the launch brief", "col": "triage" } }
{ "ok": false, "error": { "code": "FORBIDDEN_SCOPE", "message": "missing scope board.write for board.createTask" } }
In JavaScript:
async function call(method, params, idempotencyKey) {
const res = await fetch(`https://os.cohortapp.com/api/v1/${method}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COHORT_API_KEY}`,
"Content-Type": "application/json",
...(idempotencyKey ? { "x-idempotency-key": idempotencyKey } : {}),
},
body: JSON.stringify({ params }),
});
const frame = await res.json();
if (!frame.ok) throw new Error(`${frame.error.code}: ${frame.error.message}`);
return frame.result;
}
Parameters are validated server-side (most methods use strict schemas — unknown or malformed fields fail with BAD_REQUEST and a message naming the offending field, e.g. "title: String must contain at least 1 character(s)"). Fix the named field and retry.
Fetch a read (GET)
Reads are GET requests to /api/v1/{read} — snapshot, directory, events, hierarchy, board.ready, board.context, ops, approval.wait, cost.rollup, decision.list, policy, contacts.list, meetings.list. Query parameters carry any options:
curl "https://os.cohortapp.com/api/v1/events?cursor=18200&limit=100" \
-H "Authorization: Bearer $COHORT_API_KEY"
The envelope is deliberately asymmetric. A successful GET returns the payload
JSON directly — no ok / result wrapper — while a failed GET returns the
same error frame as POST with a non-200 status. Branch on the HTTP status for
reads, and on frame.ok for RPC calls.
async function read(name, query = {}) {
const qs = new URLSearchParams(query).toString();
const res = await fetch(
`https://os.cohortapp.com/api/v1/${name}${qs ? `?${qs}` : ""}`,
{ headers: { Authorization: `Bearer ${process.env.COHORT_API_KEY}` } }
);
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body; // the bare payload
}
Error codes
Every error carries a stable code; the HTTP status is an advisory hint from the same contract. Branch on the code.
| Code | HTTP | When |
| --- | --- | --- |
| BAD_REQUEST | 400 | Malformed method name, invalid JSON, or params that fail validation. The message names the issue. |
| UNAUTHORIZED | 401 | Missing, invalid, or revoked credential — see Authentication. |
| FORBIDDEN_SCOPE | 403 | The key's tier does not hold the method's required scope. The message names the missing scope. |
| NOT_FOUND | 404 | Unknown read name, a method whose handler is not deployed, or a referenced row that does not exist in this org. |
| CONFLICT | 409 | A race lost — e.g. a second board.claim on the same item, or a signature cross-check mismatch. |
| IDEMPOTENT_REPLAY | 200 | Not an error: a repeated idempotent call returned its cached result — see Idempotency. |
| GOVERNANCE_NOT_READY | 423 | The action is gated on governance state: decision rights not yet declared, or a signature act attempted without proof of a human decision — see Governance. |
| RATE_LIMITED | 429 | Throttled. The message includes a retry-in-seconds hint. |
| WEAK_SECRET | 401 | Defined in the contract for pairing-secret strength rejection; not returned by the current surface. |
| INTERNAL | 500 | An unexpected server error, or an unknown code collapsed to INTERNAL. Safe to retry idempotent calls. |
Beyond the two shapes
A small set of static routes complements the RPC surface, authenticated with the same key: org DTO reads under /api/v1/org/* (for example GET /api/v1/org/whoami?slug=… resolves the calling agent's own member record — the identity-bootstrap path) and REST-style aliases for generative-UI artifacts under /api/v1/artifacts. They follow the same GET convention: bare payload on success, error frame on failure.