Documentation

Call Framed from your own stack.

Protocol 2026-08-21

Start

You call Framed from your own stack. You talk to a customer workspace without opening the Framed UI.

Mail is another door into the same chat. A workspace member can email the workspace address on mail.framed.dev; Framed answers in the thread and in Chat. Slack and Teams can use this API later as more doors into that conversation.

You call only https://www.framed.dev/api. Every workspace, memory, chat, Studio, agent, job, insight, and approval action on this page uses that host and a short path. You can create and read those objects from your own stack. A customer write is approved by a person: in Framed, or through the Platform API, as the person behind the key.

The platform objects you drive are:

  • Workspace: one customer environment.
  • Memory: governed facts the workspace holds. You read and write them at /context.
  • Chat: a thread that runs the same Framed chat path.
  • Studio item: a memo, report, or presentation, plus its public share URL.
  • Agent: an installed runner on that workspace. You list agents, then start them as jobs.
  • Job: one agent run, or one memory write that waits for approval.
  • Insight: the briefing a job leaves behind.
  • Approval: a write that still waits in Tasks.

Key

An account issues its own keys: Settings → Connections → Framed API keys. Name the key, tick the workspaces it may reach, and copy it once. Framed stores only a hash.

Building on top of Framed for several accounts at once is a different arrangement. Ask about it at partner-support@framed.dev.

Send the key on every call in one of these headers:

X-Framed-Api-Key: frk_live_…
Authorization: Bearer frk_live_…

Either header works, and the older X-Framed-Partner-Key keeps working too. You do not need a Framed browser session. A key reaches exactly the workspaces it was issued for: one, a chosen few, or every workspace on its own account. Never anything beyond that account.

A key an account issued for itself reads context and proposes facts for approval. Starting agent runs, creating documents or chats, and making share links are not on it.

First call

Check that the API is up first.

curl https://www.framed.dev/api/health

Then ask which workspaces this key can see.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/whoami

GET /workspaces returns the same list. The sections below explain each concept, then the calls. The Reference lists every method with a working curl.

Workspaces

A workspace is one customer environment. It holds memory, chats, Studio items, and installed agents. Your key only reaches workspaces it is allowed to see.

List the workspaces this key can see.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/workspaces

Get one workspace.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/workspaces/WORKSPACE_ID

If you want a new workspace on an account this key can use, send POST /workspaces. If the key can see more than one account, include agency_id.

curl -X POST https://www.framed.dev/api/workspaces \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name":"Northwind"}'

A created workspace looks like this.

{
  "ok": true,
  "workspace": {
    "id": "WORKSPACE_ID",
    "name": "Northwind",
    "agency_id": "ACCOUNT_ID",
    "status": "draft"
  }
}

If this key is pinned to one workspace, create returns 403. If the account is at its workspace limit, create returns 403 with code: "limit_reached".

Memory

Memory is the governed facts a workspace holds. Chat, Studio, and agents read it. You read and write memory through /context.

Read the current memory on a workspace.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/context?workspace_id=WORKSPACE_ID"

If you want to add or correct a memory fact, send POST /context.

curl -X POST https://www.framed.dev/api/context \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","fact":"Debtor 123 is on hold until 2026-09-01"}'

The response is 202. You get a job_id and an approval_id. Status is awaiting_approval until someone decides in Framed. The fact is not stored yet.

If you want to replace an existing fact, send that fact’s id as replace_id. After the yes the old fact is kept as history and yours is the latest truth. To add detail to a fact that stays true, send its id as extends_id instead; both stay. Never both at once. Pass kind when it is not a plain fact: durable_fact (default), preference, episode or process_rule. The replacement still waits for approval, and pointing at a fact that is already history answers 409.

GET /memory/:id (with workspace_id) returns one memory with its chain: what it replaced, what replaced it, what it extends, what was worked out from it, and is_latest_truth. The context pack carries an id on every fact, an always-on profile for the person the key acts as, and a retrieved memories section with the same ids and edges.

Chats

A chat is a thread on a workspace. You start a thread, send the next message, or read what is already there. The same Framed chat path runs the turn. Writes that change memory still wait under Tasks.

List chats on a workspace.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/chats?workspace_id=WORKSPACE_ID"

Start a chat and send the first message.

curl -X POST https://www.framed.dev/api/chats \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","message":"What do we know about Debtor 123?"}'

Continue a thread, then read it.

curl -X POST https://www.framed.dev/api/chats/CHAT_ID/messages \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"message":"Summarize the last answer."}'

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/chats/CHAT_ID

If the turn is still running after 45 seconds, the response is 202 with status: "accepted". Poll GET /chats/:id for the finished messages.

Studio

A Studio item is a draft document on a workspace: a memo, a report, or a presentation. You create it, list items, get one item, and fetch its public share URL.

List Studio items on a workspace.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/studio?workspace_id=WORKSPACE_ID"

Create a draft. Generation runs in the background. Poll GET /studio/:id until the body is filled.

curl -X POST https://www.framed.dev/api/studio \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","title":"Q3 brief","prompt":"Write a one-page memo","type":"memo"}'

Get one item, then fetch the public share URL.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/studio/STUDIO_ID

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/studio/STUDIO_ID/share

share_url is the live link on https://www.framed.dev/share/…. If a frozen publication exists, publication_url points at /p/….

Agents

An agent is a runner already installed on a workspace. You do not install agents from this API. You list the ones that are there, then start one as a job.

List installed agents on a workspace.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/agents?workspace_id=WORKSPACE_ID"

Each row gives you the slug you send to POST /jobs.

{
  "ok": true,
  "workspace_id": "WORKSPACE_ID",
  "agents": [
    {
      "slug": "news_tracker",
      "name": "News tracker",
      "description": "Watch the topics this workspace follows",
      "status": "active",
      "kind": "catalog"
    }
  ]
}

If an agent is paused, do not start it until someone turns it on in Framed.

Jobs

A job runs an agent that is already installed on that workspace. Writes the agent proposes still go through the same approval queue.

If you do not know the slug, call GET /agents?workspace_id= first.

curl -X POST https://www.framed.dev/api/jobs \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","agent_slug":"news_tracker"}'

Poll status with GET /jobs/:id, or list jobs with GET /jobs?workspace_id=WORKSPACE_ID.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/jobs/JOB_ID

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/jobs?workspace_id=WORKSPACE_ID"

When the job succeeds, it often returns an insight_id. Fetch that briefing with GET /insights/:id.

Optional HTTPS webhook (per request or stored on the key). Framed POSTs metadata only, never the fact body.

{
  "event": "job.status",
  "api_version": "2026-08-21",
  "job_id": "…",
  "workspace_id": "…",
  "kind": "context_write",
  "status": "awaiting_approval"
}

When the call used an API key, the body is signed as X-Framed-Signature: sha256=… (HMAC-SHA256 of the raw body, key = the API key). Treat the webhook as a ping and confirm with GET /jobs/:id.

Insights

An insight is a briefing a job or agent leaves on a workspace. You read insights. You do not create or dismiss them from this API.

List recent insights on a workspace.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/insights?workspace_id=WORKSPACE_ID"

Get one insight, including the markdown body.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/insights/INSIGHT_ID

List rows omit content_md. The detail call includes it. Use the list to find an id, then fetch the body.

Approvals

If a write changes what a workspace holds, Framed does not store it immediately. Framed puts the write on Insights → To review.

Why: a customer workspace must not change silently. The customer stays the owner of what the workspace holds. Someone at the customer approves or rejects it. The fact lands only after approval.

Memory writes through POST /context always wait. Chat and agent writes that change memory use the same queue. Approve & save runs the memory write. Dismiss or Deviation rejects the job. There is no silent write path.

You can read the queue here. Deciding is a person's act: someone at the customer decides in Framed, or your own system records their decision through the Platform API, as the person behind the key.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/approvals?workspace_id=WORKSPACE_ID"

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/approvals/APPROVAL_ID

Platform

Framed runs a workspace's processes end to end and stops for a person only where the process says so. The Platform API lets your own stack drive that work and read it back: start a process, follow its run, read what it produced, and decide the human-approval steps. Same key, same workspace scope, under https://www.framed.dev/api/platform.

Four nouns, the same four the MCP connector speaks:

  • Process: a written-down method the workspace runs. runnable says Framed has a plan for it; starts says whether a client starts it immediately or after_approval, which is the process's own setting under Processes.
  • Run: one execution. Status, the stage it is on, what blocks it (an approval, a question, an error), the transitions it takes now, and links to everything else.
  • Result: one thing a run produced. Either a record or list as JSON, or a file reference into Framed's own store with a signed link that lasts 15 minutes. Every result names its schema_ref: your playbook's, when a step declared one.
  • Approval: a human-approval step. List what waits, read what is being decided, approve or reject with a reason.

Every call acts as a person: the account admin who issued the key. A start is recorded as theirs; so is a decision. That is what keeps a key from being more powerful than a colleague.

GET /platform/processes?workspace_id=

The processes a workspace can run, each with its latest run. GET /platform/processes/:id reads one. Filter with query= (part of a name); limit caps at 100. Processes in a space the person cannot open are left out and counted in withheld.

{
  "ok": true,
  "processes": [{
    "id": "…", "slug": "permit-check", "name": "Permit expiry check",
    "owner": "…", "cadence": "weekly", "status": "verified",
    "runnable": true,
    "starts": "immediately",
    "schema_refs": ["nordhaven:permit_rows/2"],
    "latest_run": { "id": "…", "status": "done", "updated_at": "…" },
    "links": { "self": "…", "runs": "…/platform/processes/…/runs" }
  }]
}

POST /platform/processes/:id/runs

Starts the process the way the Start button does, with every gate that button runs. Long work is asynchronous: you get the run back with 202 and poll it until settled is true. A process set to ask a person first answers status: waiting_for_approval with the approval to watch; once approved, it carries the run_id.

curl -X POST -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"input": {"region": "north"}, "reason": "Weekly check"}' \
  https://www.framed.dev/api/platform/processes/PROCESS_ID/runs
{
  "ok": true,
  "status": "started",
  "run": {
    "id": "…",
    "status": "queued",
    "settled": false,
    "stage": null,
    "blockers": [],
    "allowed_transitions": ["cancel"],
    "links": { "self": "…/platform/runs/…", "results": "…/results", "approvals": "…" }
  }
}

GET /platform/runs?workspace_id=

Runs in a workspace, most recently updated first. Filter with process_id=, status= (comma-separated: queued, running, awaiting_input, done, error, canceled) and updated_after=. The answer echoes the filter it applied.

GET /platform/runs/:id

Poll this until settled is true. When the run waits on a person, blockers says which approval to decide or which question to answer in Framed; stage is the step it is on.

{
  "ok": true,
  "run": {
    "id": "…", "process_id": "…", "title": "Permit expiry check",
    "status": "awaiting_input", "settled": false,
    "stage": { "index": 1, "count": 3, "key": "sign_off", "title": "A person checks the exceptions", "status": "running" },
    "steps": [ { "key": "read_register", "title": "…", "status": "done" }, … ],
    "blockers": [ { "kind": "approval", "approval_id": "…", "title": "A person checks the exceptions" } ],
    "allowed_transitions": ["cancel"],
    "input": { "region": "north" },
    "links": { "self": "…", "results": "…", "approvals": "…", "transitions": "…", "process": "…" },
    "created_by": "…", "created_at": "…", "updated_at": "…", "completed_at": null
  }
}

POST /platform/runs/:id/transitions

continue or cancel, and only what the run takes now: allowed_transitions is the rule, not a menu. Queued and running runs take cancel; a run parked on a question takes both; a run waiting on a human approval takes cancel only. A bare continue is not a yes: the refusal names the approval_id to decide. There are no free-form stage writes.

curl -X POST -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"transition": "cancel", "reason": "Wrong week"}' \
  https://www.framed.dev/api/platform/runs/RUN_ID/transitions

GET /platform/runs/:id/results

Readable while the run is going and after it settles. Files and lists first, then records: findings, the approval cards the run raised, and what each step concluded. Filter with kind= or schema_ref=.

{
  "kind": "file",
  "schema_ref": "framed:document/memo",
  "process_id": "…", "run_id": "…",
  "step": { "key": "memo", "title": "Write the permit memo" },
  "file_ref": {
    "id": "document:…",
    "name": "permit-status.md",
    "media_type": "text/markdown",
    "url": "https://www.framed.dev/api/platform/files/document/…?token=…",
    "url_expires_at": "2026-09-16T09:15:00.000Z"
  }
}

Three kinds. file: a document the run wrote (a memo, a report, a deck) or an upload it referenced, with a file_ref. list: a research table, with columns and flat rows. record: a finding (content_md), an approval card the run raised (its payload, plus the approval's state), or what a step concluded. Every result carries process_id, run_id, the step it came from, and a schema_ref: the step's own when the playbook declared one, else a Framed built-in such as framed:document/memo, framed:research_table, framed:insight/briefing, framed:approval/run_gate or framed:step_conclusion.

GET /platform/results/:id reads one result by the id a list printed, with a fresh file link. A result id is <run id>.<document|insight|approval|step>.<ref> and stays valid for as long as the run does.

GET /platform/files/document/:id?token=

The document behind a file_ref, on the signed link the result handed out. No API key: the link carries its own credential and stops working 15 minutes after it was minted, so a person opening a memo from your dashboard needs nothing else. Served as text/markdown (a memo or report), text/html (a rendered deck or designed report) or application/json (a table). Uploads use a storage link instead, under the same 15-minute rule.

GET /platform/approvals?workspace_id=

Human-approval steps. status=open (default) lists what waits on a person; decided what has been answered; all both. run_id= narrows to one run's cards. GET /platform/approvals/:id reads one, with payload: what is actually being decided, as the card stores it.

{
  "ok": true,
  "approvals": [{
    "id": "…", "kind": "run_gate",
    "title": "A person checks the exceptions",
    "summary": "Two permits expire within 30 days.",
    "queue": "needs_you",
    "status": "open", "decision": null,
    "execution": { "status": "none" },
    "run_id": "…",
    "links": { "self": "…", "decision": "…/decision", "run": "…" }
  }]
}

POST /platform/approvals/:id/decision

approve or reject, with a reason (a rejection needs one). The decision is recorded as the person behind the key, exactly as the Tasks list records one, and Framed carries it out: a gate resumes its run, an approved start starts it (the card then carries run_id), a rejected card writes the standing rule. Poll the approval until execution.status is succeeded or failed. A card takes one decision; a second answers 409 already_decided with the card as it stands.

curl -X POST -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"decision": "approve", "reason": "Both renewals are already in flight."}' \
  https://www.framed.dev/api/platform/approvals/APPROVAL_ID/decision

Refusals

Every refusal is { ok: false, error, reason }: a sentence for a person, a code for a program.

StatusreasonMeaning
400invalid_input, invalid_decision, unknown_transitionThe request is malformed; the sentence says what is missing.
401Missing or invalid key.
403workspace_out_of_reachThis key or person cannot open that workspace.
403access_policyThe person lacks the grant; grant names it, where says Settings, Access.
403no_personNobody stands behind this key; it reads but cannot run or decide.
403file_token_invalid, file_token_expiredA signed file link. Fetch the result again.
404no_such_process, no_such_run, no_such_result, no_such_approval, unknown_routeNot there, or in a space this person cannot open.
409not_runnableThe process has no plan Framed can run yet.
409already_runningOne run at a time per process; run_id says which.
409transition_not_allowedThe run's state does not take it; allowed_transitions says what it does.
409already_decided, not_a_decisionThe card was decided already, or is a notice.
502start_failed, decision_failedFramed could not carry it out; error says what it said.

The full contract is in the OpenAPI document at /platform-openapi.yaml. The complete reference, every route, field and reason code, is at Platform API reference. Chat, Studio and event webhooks come to this surface later; poll for now.

Reference

Methods

GET /health, GET /whoami, GET /workspaces, POST /workspaces, GET /workspaces/:id, GET /context?workspace_id=, POST /context, GET /chats?workspace_id=, POST /chats, GET /chats/:id, POST /chats/:id/messages, GET /studio?workspace_id=, POST /studio, GET /studio/:id, GET /studio/:id/share, GET /agents?workspace_id=, POST /jobs, GET /jobs, GET /jobs/:id, GET /insights?workspace_id=, GET /insights/:id, GET /approvals?workspace_id=, GET /approvals/:id. Platform: GET /platform/processes?workspace_id=, GET /platform/processes/:id, POST /platform/processes/:id/runs, GET /platform/runs?workspace_id=, GET /platform/runs/:id, POST /platform/runs/:id/transitions, GET /platform/runs/:id/results, GET /platform/results/:id, GET /platform/approvals?workspace_id=, GET /platform/approvals/:id, POST /platform/approvals/:id/decision.

MethodPathAuthResponse
GET/healthNo200 API status
GET/whoamiYesAccount and workspaces
GET/workspacesYesWorkspace list
POST/workspacesYes201 created workspace
GET/workspaces/:idYesOne workspace
GET/context?workspace_id=YesMemory facts
POST/contextYes202 awaiting approval
GET/chats?workspace_id=YesChat list
POST/chatsYesStarted thread
GET/chats/:idYesThread and messages
POST/chats/:id/messagesYesContinued thread
GET/studio?workspace_id=YesStudio list
POST/studioYes202 generating
GET/studio/:idYesOne Studio item
GET/studio/:id/shareYesPublic share URL
GET/agents?workspace_id=YesInstalled agents
POST/jobsYes202 or 200
GET/jobsYesJob list
GET/jobs/:idYesOne job
GET/insights?workspace_id=YesInsight list
GET/insights/:idYesOne insight
GET/approvals?workspace_id=YesApproval queue
GET/approvals/:idYesOne approval

GET /health

If you want to check that the host is up, call this first. No key.

curl https://www.framed.dev/api/health
{
  "ok": true,
  "service": "framed-api",
  "api_version": "2026-08-21",
  "docs": "https://www.framed.dev/docs",
  "base": "https://www.framed.dev/api"
}

GET /whoami

If you want the account and workspaces this key can see, call this.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/whoami
{
  "ok": true,
  "api_version": "2026-08-21",
  "account": { "id": "ACCOUNT_ID", "name": "Northwind Labs" },
  "key_scoped_workspace": null,
  "workspaces": [
    { "id": "WORKSPACE_ID", "name": "Northwind", "sandbox": false }
  ]
}

GET /workspaces

If you want only the workspace list, use this instead of /whoami.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/workspaces

POST /workspaces

If you want a new workspace, send name. If the key can see more than one account, also send agency_id.

curl -X POST https://www.framed.dev/api/workspaces \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name":"Northwind","agency_id":"ACCOUNT_ID"}'

GET /workspaces/:id

If you already have a workspace id, fetch that one row.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/workspaces/WORKSPACE_ID

GET /context

If you want the current memory facts, pass workspace_id.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/context?workspace_id=WORKSPACE_ID"
{
  "ok": true,
  "workspace_id": "WORKSPACE_ID",
  "facts": [
    {
      "id": "FACT_ID",
      "content": "Debtor 123 is on hold until 2026-09-01",
      "source": "approved",
      "source_label": "Finance",
      "created_at": "2026-08-21T08:00:00.000Z",
      "updated_at": "2026-08-21T08:00:00.000Z"
    }
  ]
}

POST /context

If you want to add or correct a fact, this call always returns 202. The fact waits in Insights → To review.

curl -X POST https://www.framed.dev/api/context \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","fact":"Debtor 123 is on hold until 2026-09-01","replace_id":"FACT_ID","label":"Finance","webhook_url":"https://hooks.example/framed"}'
{
  "ok": true,
  "job_id": "JOB_ID",
  "approval_id": "APPROVAL_ID",
  "status": "awaiting_approval",
  "write": "queued_for_approval"
}

GET /chats

If you want threads on one workspace, pass workspace_id.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/chats?workspace_id=WORKSPACE_ID"

POST /chats

If you want a new thread, send the first message. Optional title.

curl -X POST https://www.framed.dev/api/chats \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","message":"What do we know about Debtor 123?","title":"Debtor 123"}'

GET /chats/:id

If you want the messages on a thread, fetch it by id.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/chats/CHAT_ID

POST /chats/:id/messages

If you want to continue a thread, send the next message.

curl -X POST https://www.framed.dev/api/chats/CHAT_ID/messages \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"message":"Summarize the last answer."}'

GET /studio

If you want drafts on one workspace, pass workspace_id.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/studio?workspace_id=WORKSPACE_ID"

POST /studio

If you want a new draft, send title, prompt, and optional type (memo, report, or presentation).

curl -X POST https://www.framed.dev/api/studio \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","title":"Q3 brief","prompt":"Write a one-page memo","type":"memo"}'

GET /studio/:id

If you want one draft, including its body, fetch it by id.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/studio/STUDIO_ID

GET /studio/:id/share

If you want the public URL, call share. Framed mints a token if the item does not have one yet.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/studio/STUDIO_ID/share
{
  "ok": true,
  "studio_id": "STUDIO_ID",
  "share_url": "https://www.framed.dev/share/SHARE_TOKEN",
  "publication_url": null
}

GET /agents

If you want the slugs you can send to POST /jobs, list the agents installed on that workspace.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/agents?workspace_id=WORKSPACE_ID"

POST /jobs

If you want to run an installed agent, send agent_slug. Optional prompt and webhook_url.

curl -X POST https://www.framed.dev/api/jobs \
  -H "Authorization: Bearer frk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id":"WORKSPACE_ID","agent_slug":"news_tracker","prompt":"Focus on Q3","webhook_url":"https://hooks.example/framed"}'

GET /jobs

If you want recent jobs for this key, call list. Pass workspace_id to narrow the list.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/jobs?workspace_id=WORKSPACE_ID"

GET /jobs/:id

If you want one job, including insight_id and result, fetch it.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/jobs/JOB_ID

GET /insights

If you want recent briefings, pass workspace_id.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/insights?workspace_id=WORKSPACE_ID"

GET /insights/:id

If you want the markdown body, fetch one insight.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/insights/INSIGHT_ID

GET /approvals

If you want the review queue, pass workspace_id.

curl -H "Authorization: Bearer frk_live_…" \
  "https://www.framed.dev/api/approvals?workspace_id=WORKSPACE_ID"

GET /approvals/:id

If you want one queued write, including the proposed fact, fetch it.

curl -H "Authorization: Bearer frk_live_…" \
  https://www.framed.dev/api/approvals/APPROVAL_ID

Fields

POST /workspaces: name (required), agency_id when the key can see more than one account.

POST /context: workspace_id (required), fact (required), replace_id or extends_id (not both), kind, label, webhook_url (https only).

GET /memory/:id: workspace_id (required).

POST /chats: workspace_id (required), message (required), title.

POST /chats/:id/messages: message (required).

POST /studio: workspace_id (required), title (required), prompt (required), type (memo, report, or presentation).

POST /jobs: workspace_id (required), agent_slug (required), prompt, webhook_url (https only).

Statuses

queued, awaiting_approval, running, succeeded, failed, rejected, generating, accepted.

Errors

401 if the key is missing or invalid. 403 if the key is inactive or cannot see this workspace. 400 if a required field is missing. 404 if the job, chat, Studio item, insight, or route is unknown. Unknown routes point to https://www.framed.dev/docs.

Write kinds

A memory write waits as partner_write on v2_approvals until someone decides in Insights → To review.

Isolation

  • A key only reaches workspaces it is allowed to see.
  • A key never reads or writes another developer’s workspaces, or workspaces this key cannot access.
  • Workspace data stays in the EU (Stockholm).
  • Plaintext keys are never stored. Only a SHA-256 hash is kept.