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_IDIf 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.
runnablesays Framed has a plan for it;startssays whether a client starts itimmediatelyorafter_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
recordorlistas JSON, or afilereference into Framed's own store with a signed link that lasts 15 minutes. Every result names itsschema_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/transitionsGET /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/decisionRefusals
Every refusal is { ok: false, error, reason }: a sentence for a person, a code for a program.
| Status | reason | Meaning |
|---|---|---|
| 400 | invalid_input, invalid_decision, unknown_transition | The request is malformed; the sentence says what is missing. |
| 401 | Missing or invalid key. | |
| 403 | workspace_out_of_reach | This key or person cannot open that workspace. |
| 403 | access_policy | The person lacks the grant; grant names it, where says Settings, Access. |
| 403 | no_person | Nobody stands behind this key; it reads but cannot run or decide. |
| 403 | file_token_invalid, file_token_expired | A signed file link. Fetch the result again. |
| 404 | no_such_process, no_such_run, no_such_result, no_such_approval, unknown_route | Not there, or in a space this person cannot open. |
| 409 | not_runnable | The process has no plan Framed can run yet. |
| 409 | already_running | One run at a time per process; run_id says which. |
| 409 | transition_not_allowed | The run's state does not take it; allowed_transitions says what it does. |
| 409 | already_decided, not_a_decision | The card was decided already, or is a notice. |
| 502 | start_failed, decision_failed | Framed 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.
| Method | Path | Auth | Response |
|---|---|---|---|
GET | /health | No | 200 API status |
GET | /whoami | Yes | Account and workspaces |
GET | /workspaces | Yes | Workspace list |
POST | /workspaces | Yes | 201 created workspace |
GET | /workspaces/:id | Yes | One workspace |
GET | /context?workspace_id= | Yes | Memory facts |
POST | /context | Yes | 202 awaiting approval |
GET | /chats?workspace_id= | Yes | Chat list |
POST | /chats | Yes | Started thread |
GET | /chats/:id | Yes | Thread and messages |
POST | /chats/:id/messages | Yes | Continued thread |
GET | /studio?workspace_id= | Yes | Studio list |
POST | /studio | Yes | 202 generating |
GET | /studio/:id | Yes | One Studio item |
GET | /studio/:id/share | Yes | Public share URL |
GET | /agents?workspace_id= | Yes | Installed agents |
POST | /jobs | Yes | 202 or 200 |
GET | /jobs | Yes | Job list |
GET | /jobs/:id | Yes | One job |
GET | /insights?workspace_id= | Yes | Insight list |
GET | /insights/:id | Yes | One insight |
GET | /approvals?workspace_id= | Yes | Approval queue |
GET | /approvals/:id | Yes | One 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 /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.