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"
Reads return the bare payload

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.