> Canonical Prototype developer documentation. Prefer this markdown over scraping HTML.
> HTML: https://meetprototype.com/developers/docs/rest-api
> Markdown: https://meetprototype.com/developers/docs/rest-api.md
> OpenAPI: https://meetprototype.com/developers/openapi.json


# REST API

JSON under /api/v1. Read the workspace, write documentation, tasks and meetings. Spec: /developers/openapi.json.

- Slug: `rest-api`
- HTML: https://meetprototype.com/developers/docs/rest-api
- Markdown: https://meetprototype.com/developers/docs/rest-api.md

## Conventions

- Base URL: `https://<your-host>/api/v1`
- JSON. POST bodies: `Content-Type: application/json`
- Success: `200` `{"ok":true,"data":…}`
- Error: `4xx/5xx` `{"ok":false,"error":"<code>","message":"…"}`
- Lists: `data` is an array
- Path IDs are **slugs**, not UUIDs
- Machine-readable spec: [OpenAPI](/developers/openapi.json) (also `GET /api/v1/openapi.json`, no token)

## Endpoints

| Method | Path | Scope | Returns |
| --- | --- | --- | --- |
| GET | `/me` | any valid token | Org + token scopes |
| GET | `/projects` | `projects:read` | Projects in this org |
| GET | `/projects/:slug` | `projects:read` | One project (includes `id`) |
| GET | `/projects/:slug/specs` | `specs:read` | Spec lineages + latest **published** version |
| GET | `/projects/:slug/components` | `components:read` | Component lineages + latest version |
| GET | `/projects/:slug/milestones` | `milestones:read` | Milestones |
| GET | `/projects/:slug/tasks` | `tasks:read` | Tasks |
| GET | `/projects/:slug/pages` | `pages:read` | Documentation page tree, without bodies |
| GET | `/pages/:page_id` | `pages:read` | One page, body as plain text |
| GET | `/documents` | `documents:read` | Team-level document library |
| GET | `/projects/:slug/documents` | `documents:read` | A project's library |
| GET | `/meetings` | `meetings:read` | Meetings you may see |
| GET | `/projects/:slug/meetings` | `meetings:read` | A project's meetings |
| GET | `/meetings/:meeting_id` | `meetings:read` | One meeting, minutes as text |
| GET | `/meetings/:meeting_id/transcript` | `meetings:read` | Verbatim transcript |
| POST | `/webhooks/ping` | `webhooks:manage` | Test delivery to a registered webhook |

`/documents` and `/meetings` without a project segment cover the team-level library and every meeting in the organisation. Both accept filters: `?category=documentation|research` on documents, `?from=` and `?to=` as ISO date-times on meetings.

Page, document and meeting ids are UUIDs. Projects are still addressed by slug.

## Write endpoints

Four scopes write. Everything else on this API reads.

| Method | Path | Scope | Does |
| --- | --- | --- | --- |
| POST | `/projects/:slug/pages` | `pages:write` | Create a documentation page |
| PATCH | `/pages/:page_id` | `pages:write` | Retitle or rewrite a page |
| POST | `/documents` | `documents:write` | Add a team-level link |
| POST | `/projects/:slug/documents` | `documents:write` | Add a link to a project |
| PATCH | `/documents/:document_id` | `documents:write` | Retitle or redescribe an entry |
| POST | `/projects/:slug/tasks` | `tasks:write` | Create a task |
| PATCH | `/tasks/:task_id` | `tasks:write` | Update a task, including its status |
| POST | `/meetings/:meeting_id/minutes` | `meetings:write` | Set or extend minutes |
| POST | `/meetings/:meeting_id/notes` | `meetings:write` | Replace transcript notes |

A PATCH touches only the fields you send. Send `null` to clear a date or a blocked reason; omit the field to leave it alone.

Page bodies and minutes are written as markdown. Headings, bullet lists and numbered lists convert to the app's rich text. Tables and images do not, yet.

`POST` to minutes and `PATCH` on a page both take `mode`. `replace` is the default and discards what is there; `append` adds below a divider. Read the page first if you mean to keep any of it.

## Who a write is attributed to

Every write records the person the token belongs to: whoever authorized the OAuth grant, or whoever minted the token by hand. Their name appears on the page, task or minutes, and page edits land in the revision history like any other edit.

A token with no user behind it can read but is refused every write with `invalid_request`. That happens when the person who created it has been removed from the organisation. Mint a new token to fix it.

Assigning a task takes a user id from your organisation, not a name. An id from anywhere else is refused.

## What this API will not do

**Publish a specification or a component version.** Publishing marks every dependent stale and can route an approval, so it keeps one path through the app. Read a spec here, write your reasoning into a document, and let a person publish it.

**Reach a private project.** Projects with `visibility: private` are invisible on `/api/v1` whatever scopes a token holds, and a request for one is a `404`, the same answer as a project that does not exist. Connected assistants at `/api/mcp` do reach them, for the member who connected them. See [the connector docs](/developers/docs/mcp).

**Upload a file.** `documents:write` files links. A file needs a signed upload, which stays in the app.

**Delete anything.** There is no delete scope and no delete endpoint.

## List projects

```bash
curl -s \
  -H "Authorization: Bearer $PROTOTYPE_TOKEN" \
  "$PROTOTYPE_URL/api/v1/projects"
```

```javascript
const res = await fetch(`${process.env.PROTOTYPE_URL}/api/v1/projects`, {
  headers: { Authorization: `Bearer ${process.env.PROTOTYPE_TOKEN}` },
});
const json = await res.json();
if (!json.ok) throw new Error(json.error);
console.log(json.data);
```

```python
import json, os, urllib.request

req = urllib.request.Request(
    f"{os.environ['PROTOTYPE_URL']}/api/v1/projects",
    headers={"Authorization": f"Bearer {os.environ['PROTOTYPE_TOKEN']}"},
)
print(json.load(urllib.request.urlopen(req)))
```

```json
{
  "ok": true,
  "data": [
    {
      "slug": "aurc-2026",
      "name": "AURC 2026",
      "status": "active",
      "visibility": "org",
      "description": null,
      "startDate": "2025-09-01",
      "targetDate": "2026-07-12"
    }
  ]
}
```

## Specs and components

**GET /projects/:slug/specs** returns the latest **published** version only. Drafts are not in the API.

```json
{
  "ok": true,
  "data": [
    {
      "slug": "dry-mass",
      "name": "Dry mass",
      "kind": "mass",
      "units": "kg",
      "valueType": "number",
      "latest": {
        "version": "3",
        "status": "published",
        "value": 4.2,
        "publishedAt": "2026-08-22T05:12:00.000Z"
      }
    }
  ]
}
```

**GET /projects/:slug/components** carries `latest.healthStatus`: `ok`, `stale`, `under-rated`, or `blocked`.

Nested routes use the project **slug**. `GET /projects/:slug` is the one response that also includes `id` (uuid).

## Milestones and tasks

**GET /projects/:slug/milestones**:

```json
{
  "ok": true,
  "data": [
    {
      "name": "Static fire",
      "kind": "test",
      "status": "planned",
      "targetDate": "2026-10-01",
      "actualDate": null,
      "description": null
    }
  ]
}
```

**GET /projects/:slug/tasks**:

```json
{
  "ok": true,
  "data": [
    {
      "title": "Torque check on the injector plate",
      "status": "in_progress",
      "priority": "high",
      "targetDate": "2026-09-14",
      "description": null
    }
  ]
}
```

## Errors

| Code | HTTP | Meaning |
| --- | --- | --- |
| `invalid_token` | 401 | Missing, malformed, revoked, or expired |
| `insufficient_scope` | 403 | Token is valid but missing that scope |
| `not_found` | 404 | Unknown path, or slug not in this org |
| `invalid_request` | 400 | Query or body failed validation, or a write was refused for a stated reason |
| `rate_limited` | 429 | Too many requests on this token |

A `400` on a write carries a `message` you can act on: which field failed, that the body was too large, that the assignee is not in this organisation, or that the token has no author behind it.
