# Prototype API

REST at `/api/v1` (bearer token) plus signed webhooks. Quick start: `/developers/docs/overview`. Spec: `/developers/openapi.json`.

- Index: https://meetprototype.com/llms.txt
- Full dump: https://meetprototype.com/llms-full.txt
- OpenAPI: https://meetprototype.com/developers/openapi.json

---

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


# Quick start

Mint a bearer token, GET /api/v1/me and /projects, optionally subscribe to spec.published.

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

## What you get

REST at `/api/v1`: read projects, specs, components, milestones, tasks, documentation pages, documents and meetings, and write documentation, tasks and meeting minutes. Plus optional HMAC-signed HTTPS webhooks.

**Official modules** (`openrocket`, `github`, `kicad`, calendar, Outline) are first-party UI already in the app: `/projects/:slug/rockets`, `/repos`, `/boards`.

**Community listings have no in-app UI.** No iframe, no nav slot, no settings panel you render. Operator surfaces are the catalog, install row, and **Admin → Integrations** (tokens, webhooks). Your UI is whatever you ship: Slack, CLI, CI, your own site.

OpenAPI: [/developers/openapi.json](/developers/openapi.json). Each HTML page has a `.md` twin.

## 1. Create a token

In Prototype: **Admin → Integrations → API tokens → New token**.

For a first request, grant `projects:read`. Add `components:read` / `specs:read` when you need those collections.

Copy `ptk_live_…` now. It is shown once.

```bash
export PROTOTYPE_URL="https://<your-host>"
export PROTOTYPE_TOKEN="ptk_live_…"
```

## 2. Call the API

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

You should see `ok: true` and the org the token belongs to. Then:

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

Same call in JavaScript:

```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);
```

Next: [REST API](/developers/docs/rest-api) for every endpoint, or [the Slack example](/developers/docs/example) for a full script.

## 3. Optional: webhooks

If you want a push when a spec is published: **Admin → Integrations → Webhooks**, URL `https://your-host/prototype/hooks`, event `spec.published`. Copy `whsec_…` once.

Verify the signature on the raw body before you trust the payload. See [Webhooks](/developers/docs/webhooks).

---

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


# Authentication

Bearer token on every request. Minted in Admin → Integrations, shown once.

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

## Header

```http
Authorization: Bearer ptk_live_…
```

The token is tied to one organisation. Scopes on the token decide which collections it can read. See [Scopes](/developers/docs/scopes).

## Create and rotate

**Admin → Integrations → API tokens → New token.** Name it, pick scopes, optionally bind it to a marketplace listing.

The full value is shown once. Prototype stores a SHA-256 hash. Lost it → mint a new one and revoke the old.

Revoke is immediate: subsequent calls return `401` `invalid_token`. Mint the replacement first, switch your process over, then revoke.

## Storing the token

Keep `ptk_live_…` in an environment variable or a secret manager. Do not commit it, log the whole value, or paste it into a marketplace listing.

One token per integration is easier to revoke. Grant only the scopes that integration uses.

---

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


# OAuth

Let an addon ask an organisation for access instead of asking an admin to paste a token.

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

## When to use it

Use OAuth when other organisations install your addon. They approve it on a consent screen that names you and lists the scopes, and your addon receives the token directly.

Use a hand-minted token when the integration is for your own organisation only. See [Authentication](/developers/docs/authentication).

## 1. Register a client

**Addons → Develop → your listing → OAuth client.** Add every redirect URI you use and create the client.

The client secret is shown once. Redirect URIs are matched exactly, so `https://app.example/cb` will not accept `https://app.example/cb/done`. HTTPS only, except `localhost` and `127.0.0.1` for development.

## 2. Send the member to the consent screen

```http
GET /oauth/authorize
  ?client_id=ptc_…
  &redirect_uri=https://app.example/cb
  &response_type=code
  &scope=projects:read specs:read
  &state=<random per request>
```

`state` is yours to generate and check on the way back. Only an org admin can approve, and only scopes the API defines are accepted.

For a public client, add `code_challenge` and `code_challenge_method=S256`.

## 3. Handle the redirect

On approval the member returns to your redirect URI with `code` and your `state`.

```
https://app.example/cb?code=ptac_…&state=<yours>
```

On refusal you get `error=access_denied` and the same `state`. Compare the returned `state` to the one you issued before doing anything else.

## 4. Exchange the code

```bash
curl -X POST https://<your-host>/api/v1/oauth/token \
  -d grant_type=authorization_code \
  -d code=ptac_… \
  -d redirect_uri=https://app.example/cb \
  -d client_id=ptc_… \
  -d client_secret=ptcs_…
```

```json
{ "access_token": "ptk_live_…", "token_type": "Bearer", "scope": "projects:read specs:read" }
```

Codes are single use and expire in minutes. `redirect_uri` must match the one you authorized with. With PKCE, send `code_verifier` instead of `client_secret`.

The token works on every `/api/v1` endpoint exactly like a hand-minted one.

## Errors

Failures return an OAuth error object with a `400`, or `401` for client authentication.

| `error` | Means |
| --- | --- |
| `invalid_client` | Unknown client, or the secret did not match |
| `invalid_grant` | Code expired, already used, issued to another client, or the redirect did not match |
| `invalid_request` | A required parameter is missing |
| `unsupported_grant_type` | Only `authorization_code` is supported |
| `access_denied` | The member cancelled |

A `429` means you are sending too fast. Honour the `Retry-After` header and back off rather than retrying immediately.

## Revocation

An admin can revoke a grant from **Admin → Integrations → Authorized addons**. That immediately kills every token the grant issued, and calls start returning `401`.

Treat a sudden `401` as a revoked grant and start the flow again rather than retrying.

## Before you publish

Publishing requires a contact email and acceptance of the [publisher agreement](/developers/agreement). The email is private, and we use it when your addon starts failing or gets reported.

---

> 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.

---

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


# Webhooks

HTTPS POST to your URL when a matching change is recorded. HMAC-signed; verify before you act.

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

## Register

**Admin → Integrations → Webhooks.** Public HTTPS URL. Subscribe to the events you need ([Events](/developers/docs/events)).

Copy `whsec_…` once.

Each delivery is a POST:

| Header | Value |
| --- | --- |
| `X-Prototype-Event` | e.g. `spec.published` |
| `X-Prototype-Delivery` | uuid, safe to use as an idempotency key |
| `X-Prototype-Timestamp` | unix seconds |
| `X-Prototype-Signature` | `sha256=<hex>` of `{timestamp}.{raw_body}` |

Respond **2xx** within 10 seconds. Anything else is retried on the daily pass.

URL must be HTTPS (HTTP localhost is allowed outside production). No credentials in the URL.

## Verify

Use the **raw body** (do not JSON.parse then stringify). Compare in constant time:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, timestamp, rawBody, signatureHeader) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}
```

Reject if the signature fails, or if the timestamp is more than five minutes off. Skip duplicate `X-Prototype-Delivery` ids.

## Payload

```json
{
  "id": 1842,
  "type": "spec.published",
  "action": "published",
  "target_kind": "specification",
  "target_id": "0f3c…",
  "actor_id": "user_…",
  "occurred_at": "2026-08-22T05:12:00.000Z",
  "diff": { "before": { "value": 3.8 }, "after": { "value": 4.2 } }
}
```

`type` is the event you subscribed to. `target_kind` and `diff` differ by event; Events shows an example for each. There is no project slug on the payload. List `/projects`, then the nested collections, if you need to fan out.

**Ping:** `POST /api/v1/webhooks/ping` with `{"webhook_id":"<uuid>"}` (scope `webhooks:manage`) sends a synthetic `change.created`.

---

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


# Scopes

Which collections a token can read. Request only what your integration uses.

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

## Available scopes

| Scope | Grants |
| --- | --- |
| `org:read` | Organisation name and slug on `GET /me` |
| `projects:read` | List and read projects |
| `specs:read` | Spec lineages and the latest published version |
| `components:read` | Component lineages and the latest version |
| `milestones:read` | Project milestones |
| `tasks:read` | Tasks |
| `documents:read` | The document and research library |
| `pages:read` | Project documentation pages |
| `meetings:read` | Meetings, minutes and meeting notes |
| `webhooks:manage` | Ping a registered webhook |

Typical read loop: `projects:read` + `components:read` (add `specs:read` if you need values). `webhooks:manage` only if you ping from the integration.

A marketplace listing can declare `requested_scopes` so an admin sees what you need. Declaring them does not mint a token.

## Write scopes

Four scopes change data. They work on `/api/v1` and on the MCP connector alike.

| Scope | Grants |
| --- | --- |
| `pages:write` | Create and edit documentation pages |
| `documents:write` | Add and edit library entries |
| `tasks:write` | Create and update tasks |
| `meetings:write` | Write meeting minutes and meeting notes |

A write runs as the person who authorized the token, and the row records them as its author. A token with no user behind it can read but is refused every write.

Nothing publishes a specification or a component version. Publishing fans staleness out to dependents and can route an approval, so it stays in the app. See [the REST reference](/developers/docs/rest-api).

---

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


# Events

Webhook event names, when they fire, and what the payload looks like for each.

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

## Event names

| Event | Fired when |
| --- | --- |
| `spec.published` | A specification version is published (downstream components go stale) |
| `component.published` | A component version is published |
| `milestone.updated` | A milestone is created or edited |
| `task.updated` | A task is created or edited |
| `change.created` | Any other change event (comments, approvals, …) |

Subscribe to `spec.published` if you care about stale fan-out. Use `change.created` only if you want everything. A webhook subscribed to both gets the specific event, not a duplicate catch-all.

## Payload by event

Every delivery uses the same envelope: `id`, `type`, `action`, `target_kind`, `target_id`, `actor_id`, `occurred_at`, `diff`. `target_kind` and the shape of `diff` depend on the event.

`spec.published`, `target_kind: "specification"`:

```json
{ "diff": { "before": { "value": 3.8 }, "after": { "value": 4.2 } } }
```

`component.published`, `target_kind: "component"`:

```json
{ "diff": { "before": { "healthStatus": "stale", "version": "3" }, "after": { "healthStatus": "ok", "version": "4" } } }
```

`milestone.updated`, `target_kind: "milestone"`:

```json
{ "diff": { "before": { "status": "planned" }, "after": { "status": "complete" } } }
```

`task.updated`, `target_kind: "task"`:

```json
{ "diff": { "before": { "status": "in_progress" }, "after": { "status": "done" } } }
```

`change.created` covers everything else, so `target_kind` (`comment`, `approval_request`, …) and `diff` vary with what changed. Treat it as a signal to refetch the resource, not something to parse the shape of.

---

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


# Example: Slack on stale

A Node server that verifies webhooks, lists stale components, and posts to Slack.

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

## Setup

1. Token with `projects:read` + `components:read`.
2. Webhook URL `https://your-host/prototype/hooks`, event `spec.published`, secret `whsec_…`.
3. A Slack incoming-webhook URL.

Env: `PROTOTYPE_URL`, `PROTOTYPE_TOKEN`, `PROTOTYPE_HOOK`, `SLACK_WEBHOOK`.

## server.mjs

```javascript
import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const TOKEN = process.env.PROTOTYPE_TOKEN;
const HOOK_SECRET = process.env.PROTOTYPE_HOOK;
const BASE = process.env.PROTOTYPE_URL;
const SLACK = process.env.SLACK_WEBHOOK;

function verify(secret, timestamp, raw, signature) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.${raw}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

async function staleComponents() {
  const headers = { Authorization: `Bearer ${TOKEN}` };
  const projects = await fetch(`${BASE}/api/v1/projects`, { headers })
    .then((r) => r.json());
  const stale = [];
  for (const p of projects.data ?? []) {
    const comps = await fetch(
      `${BASE}/api/v1/projects/${p.slug}/components`,
      { headers },
    ).then((r) => r.json());
    for (const c of comps.data ?? []) {
      if (c.latest?.healthStatus === "stale") {
        stale.push(`${p.slug}/${c.slug}`);
      }
    }
  }
  return stale;
}

http.createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/prototype/hooks") {
    res.writeHead(404); res.end(); return;
  }
  const chunks = [];
  for await (const ch of req) chunks.push(ch);
  const raw = Buffer.concat(chunks).toString("utf8");
  const ts = req.headers["x-prototype-timestamp"];
  const sig = req.headers["x-prototype-signature"];
  if (!verify(HOOK_SECRET, ts, raw, sig)) {
    res.writeHead(401); res.end(); return;
  }
  const event = JSON.parse(raw);
  if (event.type === "spec.published") {
    const stale = await staleComponents();
    if (stale.length && SLACK) {
      await fetch(SLACK, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          text: `Spec published. ${stale.length} stale: ${stale.join(", ")}`,
        }),
      });
    }
  }
  res.writeHead(200); res.end("ok");
}).listen(8787);
```

Pull-only variant (no webhook):

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

---

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


# Claude and ChatGPT

Connect an assistant over MCP so it can read specs and write documentation, tasks and minutes.

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

## What the connector does

Prototype is an MCP server at `/api/mcp`. Add it to Claude or ChatGPT and the assistant can read your projects, specifications, components, milestones, tasks, documentation and meetings, and write back documentation pages, library entries, tasks, meeting minutes and meeting notes.

Both vendors speak the same protocol, so one endpoint serves both. Nothing runs inside Prototype and no model provider key is stored here: the assistant is the client, and this app is the server it calls.

Specifications are the reason to connect one. An assistant answering from a number pasted into a document is guessing. An assistant that calls `list_specs` is reading the agreed value.

## Connecting

Two decisions, made by two people.

An owner decides **whether** this organisation allows assistants at all, and how far one may go, under Admin, then Integrations. That is a decision about the organisation's data reaching a model provider, so it is made once, by someone who can make it.

Every member then decides **that this assistant acts as them**, from Settings, then Your assistant. No admin is involved in that second step. The connection reaches exactly what that member reaches, writes under their name, and cannot exceed the ceiling the owner set.

Which mechanism you use is decided by the client, not by you. **claude.ai and ChatGPT accept OAuth only**; neither has a field for a bearer token or a custom header. **Claude Code and Claude Desktop take a header directly**, so they use a token and no browser flow.

## Connecting claude.ai or ChatGPT

Add a custom connector pointing at `https://your-prototype-host/api/mcp`. There is nothing else to copy.

Your assistant registers itself, discovers the authorization endpoints from `/.well-known/oauth-authorization-server`, and sends you to Prototype to approve it. The consent screen names the assistant, shows exactly what it will be granted, and shows anything it asked for that your organisation does not allow.

Approving is what names you as the person the connection acts for. If someone else on your team connects the same assistant, that is their own connection: yours is unaffected.

## Connecting Claude Code or Claude Desktop

Create a token from Settings, then Your assistant. You do not need an admin for this, and the list of things you can grant it is whatever your organisation allows.

```bash
claude mcp add --transport http prototype \
  https://your-prototype-host/api/mcp \
  --header "Authorization: Bearer $PROTOTYPE_TOKEN"
```

A token is bound to one organisation for its whole life. If you belong to more than one, create a token in each and add a connector per organisation. Nothing follows your active organisation around, which is deliberate: an assistant that changed tenant underneath you would be worse than one that cannot.

## For addon publishers

A publisher's own OAuth client is still registered by hand at Addons, Develop, and approving one is still an owner's decision, because an addon acts for the whole organisation rather than for one person.

Assistants that register themselves through `/api/v1/oauth/register` are a separate kind of client. They are bound to the official assistants listing, can never hold publish rights, and grant nothing until a member approves them inside their organisation's ceiling.

## Tools

Each tool needs one scope. The assistant is shown only the tools its token covers, so a read-only token advertises no way to write.

| Tool | Scope |
| --- | --- |
| `list_projects` | `projects:read` |
| `list_specs`, `list_components` | `specs:read`, `components:read` |
| `list_milestones`, `list_tasks` | `milestones:read`, `tasks:read` |
| `list_pages`, `get_page` | `pages:read` |
| `list_documents` | `documents:read` |
| `list_meetings`, `get_meeting`, `get_meeting_transcript` | `meetings:read` |
| `create_page`, `update_page` | `pages:write` |
| `add_document_link`, `update_document` | `documents:write` |
| `create_task`, `update_task` | `tasks:write` |
| `write_meeting_minutes`, `write_meeting_notes` | `meetings:write` |

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

`update_page` and `write_meeting_minutes` take a `mode`. `replace` overwrites, `append` adds below a divider. Replace discards what is there, so read the page first.

## What an assistant can reach

A token acts as the person who authorized it, and sees exactly what they see. Org-visible projects are readable; a private project is readable only if that person is a member of it. A restricted meeting stays invisible unless they were entitled to it.

Every write records that person as the author. Their name appears on the page, the task or the minutes, and page edits land in the revision history like any other edit. Nothing an assistant writes is anonymous, and nothing it writes is exempt from the history.

Meeting notes written this way are marked as human-authored, so transcript jobs will not overwrite them later.

## What it cannot do

No tool publishes a specification or a component version. Publishing marks every dependent stale and can route an approval, and that has to keep one path through the app so the fan-out happens exactly once. An assistant can read a spec, propose a change in prose, and write it into a document. A person publishes it.

No tool uploads a file. `add_document_link` files a link; uploading a document goes through the app.

No tool deletes anything. There is no delete scope.

Tokens do not expire. Revoke one at Admin, Integrations when an assistant no longer needs access.

---

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


# Publishing

Optional. List the integration on the marketplace so other teams can find it.

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

## Create a listing

**Addons → Develop → New listing** (publish permission: admins, team leads, project leads).

Required: name, slug, tagline, category, description. Optional: homepage, docs, source, support, webhook URL, requested scopes, requested events.

Slugs are globally unique kebab-case. Official names (`github`, `openrocket`, …) are reserved.

- **Public**: anyone can see it in the marketplace
- **This org**: only your organisation

Install records that an org uses you. It does not mint a token and it does not mount UI in Prototype. They still create a token under Admin → Integrations. Your product UI stays on your side.

---
