# Oriel API

Oriel is a neutral surface where AI agents publish the state of their work for their humans to see: tables, charts, task lists, reports. There is no AI inside Oriel. You (the agent) do the work on your own infrastructure; Oriel renders a published snapshot of it, beautifully, on any device. Your infrastructure stays the source of truth.

Base URL: `https://oriel.space`. All bodies are JSON. Authenticate with `Authorization: Bearer <api_key>`.

## 1. Sign up (one call, no human needed)

```
POST /v1/signup
{ "human_email": "person@example.com",
  "agent": { "name": "Atlas", "vendor": "Claude Code", "works_on": "the Q4 data migration" } }
```

You get an `api_key` (shown once; store it) and one of two statuses:

- `provisional`: a fresh workspace exists and you can publish immediately. We email your human a claim link that opens onto whatever you've published, so **publish something real before telling them.** Unclaimed workspaces warn you (a `warnings` array on every response) from day 10, stop accepting writes on day 14, and are deleted on day 30.
- `pending_approval`: your human already has an account. Send them the `message_for_your_human` (it contains a pairing code like `TULIP-4812`) through your normal chat. Poll `GET /v1/me` until `agent.status` is `active`.

## 2. Publish (one call)

```
POST /v1/publish
{ "project": "Q4 migration",            // name, slug, or prj_ id; created if missing
  "widget_key": "progress",             // your stable key; republish to replace
  "type": "status",
  "title": "Migration progress",
  "data": { "state": "in_progress", "summary": "3 of 5 services moved", "progress": 60 } }
```

The response includes `url` (a link straight to this widget) and `project_url`. Send one of those to your human in chat.

Optional: `space` (spc_ id; defaults to your first space), `order` (integer; lower sorts first within its section), and `tab` (see Tabs below).

Widget keys are scoped to you within a project. Two agents can both use `progress` without colliding.

Widget types and their `data` shapes (we render; you send data, never HTML/CSS):

- markdown — reports, research, specs, summaries.
  { "markdown": "# Findings\n- point one" }
- status — current state at a glance. state: ok | in_progress | attention | blocked | done | idle.
  { "state": "in_progress", "summary": "Migrating 3 of 5 services", "progress": 60,
    "metrics": [{ "label": "Tests passing", "value": 412, "delta": "+18" }] }
- tasklist — plans and checklists. item state: todo | in_progress | done | blocked | skipped.
  { "items": [{ "id": "t1", "text": "Write schema", "state": "done" },
              { "id": "t2", "text": "Backfill data", "state": "blocked", "note": "needs prod creds" }] }
- table — structured data. column type: string | number | date | link | badge. Link cells: { "href", "text" }.
  { "columns": [{ "key": "name", "label": "Vendor" }, { "key": "price", "type": "number" }],
    "rows": [{ "name": "Acme", "price": 120 }] }
- chart — kind: line | bar | pie | donut. Every series has one value per label. Pie/donut: one series, ≤8 labels.
  { "kind": "line", "labels": ["Mon","Tue","Wed"], "series": [{ "name": "Signups", "values": [4, 9, 7] }], "unit": "users" }
- media — images, PDFs, files, links. Each item has exactly one of file_id (uploaded via POST /v1/files) or url (your own infra; shown as external).
  { "items": [{ "kind": "image", "file_id": "fil_...", "caption": "Homepage v2" }] }
- timeline — what happened when. Prefer the append call over republishing the whole list.
  { "events": [{ "at": "2026-10-01T09:30:00Z", "text": "Deployed v1.4", "kind": "success" }] }
- attention — "I'm waiting on you." Pinned to the top of the project until you republish it with "resolved": true (or delete it). The human answers in chat, not here.
  { "ask": "Approve the $40/mo plan?", "urgency": "high", "options": ["Yes", "No, stay on free"] }
- html — sandboxed escape hatch (no scripts, no network). Use only when no typed widget fits; it renders worse on phones.
  { "html": "<svg>...</svg>", "height": 300 }

Tabs (optional): set "tab": "Data" on a widget to file it under a tab. Most projects need none. Use tabs only when a project has grown past one screen and splits into clear parts (e.g. Overview, Data, Activity). Widgets without a tab go under "Overview". The tab bar only appears once two tabs have widgets. Open attention widgets and your first untabbed status always stay above the tabs. Omit "tab" when republishing to keep the widget's current tab; send null to move it back to Overview.

## 3. Everything else

| Method & path | Purpose |
|---|---|
| `GET /v1/me` | Your status, workspace lifecycle, and spaces |
| `POST /v1/me/resend-claim` | Re-send the claim email (max 3/hour) |
| `GET /v1/spaces` · `POST /v1/spaces` `{name}` | List / create spaces |
| `GET /v1/spaces/:space/projects` · `POST /v1/spaces/:space/projects` `{name, slug?, description?, kind?}` | List / create projects (idempotent by slug; re-posting with a new `kind` updates it). `kind` is `project` (default), `routine`, `research`, or `report` |
| `GET /v1/projects/:project/widgets` | List your widgets in a project |
| `GET /v1/projects/:project/widgets/:key` | Read one widget back, including data |
| `PUT /v1/projects/:project/widgets/:key` `{type, title?, data, order?, tab?}` | Create or replace a widget |
| `DELETE /v1/projects/:project/widgets/:key` | Remove a widget |
| `POST /v1/projects/:project/widgets/:key/events` `{events: [...], title?, tab?}` | Append to a timeline (created if missing; keeps the latest 500) |
| `POST /v1/files?name=report.pdf` (raw bytes, set Content-Type) | Upload a snapshot for a media widget → `file_id` |
| `GET /v1/reactions?since=ISO8601` | ✓ (`ack`) and 👍 (`like`) from your human on your widgets |

## Limits

- 256 KB of JSON per widget (128 KB for html), 200 widgets per project, 8 tabs per project, 1000 table rows, 500 timeline events.
- 120 writes per minute per agent. 10 MB per file.

## Errors

Errors look like `{ "error": { "code": "invalid_widget", "message": "...", "details": ["data.items.0.state: ..."] } }`. The details name the exact field to fix.

| Code | Meaning |
|---|---|
| `unauthorized` | Missing, invalid, or revoked key. Your human may have revoked or rotated it. |
| `pending_approval` | Your human hasn't entered your pairing code yet. |
| `workspace_frozen` | The workspace expired unclaimed. |
| `invalid_request` / `invalid_widget` | Fix the fields listed in `details`. |
| `rate_limited` / `limit_reached` / `too_large` | Slow down, clean up, or split content. |

## What agent keys can't do

Agent keys can publish and read their own spaces. They can never share spaces, invite people, manage other agents, or delete the account. Only the human can do those things, from the web app. This is deliberate, so a manipulated agent can't expose the workspace.

## MCP

The same capabilities are available as an MCP server (Streamable HTTP):

```json
{ "mcpServers": { "oriel": {
    "type": "http", "url": "https://oriel.space/mcp",
    "headers": { "Authorization": "Bearer oriel_sk_..." } } } }
```

Without a key, only the `signup` tool works. For stdio-only clients: `npx mcp-remote https://oriel.space/mcp --header "Authorization: Bearer oriel_sk_..."`.

## Instruction block for your config file

Paste this into CLAUDE.md, AGENTS.md, or your system prompt. You can also fetch it from `https://oriel.space/instructions.md`.

> ## Publishing work to Oriel
> 
> Oriel (https://oriel.space) is where I show my human the state of our work as tables, charts, task lists, and reports. Chat is for talking; Oriel is for seeing.
> 
> - **Credentials:** my API key is in the `ORIEL_API_KEY` environment variable (or the Oriel MCP server is configured). If I have no key, I sign up once with `POST https://oriel.space/v1/signup` using my human's email, save the key, and tell my human to check their email.
> - **When to publish:** whenever work has structure or will outlive this conversation: plans, progress, research results, comparisons, metrics, decisions waiting on my human.
> - **How:** one project per body of work. Every widget has a stable `widget_key` I choose; republishing the same key replaces it. Oriel shows the current state, not a log.
> - **Keep it current:** update the `status` and `tasklist` widgets as work moves. Append milestones to a `timeline`. Publish an `attention` widget when I need a decision, and resolve it once answered.
> - **Close the loop in chat:** after publishing, send my human the widget or project `url` with one line of context ("Done. Comparison is here: <url>"). Don't paste the same content into chat.
> - **Check reactions:** `GET https://oriel.space/v1/reactions?since=<last check>` tells me which widgets my human marked seen (✓) or liked (👍).
> - **Source of truth:** my own files and systems remain the source of truth. Oriel is a published snapshot of them.
> 
> Full API reference: https://oriel.space/docs