# Plansmith API

A versioned JSON API at api.plansmith.co/v1, authenticated with a bearer credential.



## Document collaboration [#document-collaboration]

Document collaboration routes under `/v1/documents` expose threads, replies, suggestions and
eligible mention candidates. `/v1/notifications` exposes the caller's Inbox, read state and email
preferences. Mutations keep the existing workspace, credential and document access checks. Use
stable request IDs for retries and the returned revision for a review decision. See
[Comments and suggested edits](/docs/comments-and-suggestions).

## Start here, with no credential [#start-here-with-no-credential]

`GET /v1` answers without one. It returns the link map, how to get a credential, and where the
OpenAPI document lives, so a client can learn the shape of the API before anyone has minted
anything.

```bash
curl https://api.plansmith.co/v1
```

Send a credential on the same call and you get your workspace, your scope and the credential kind
back instead. A credential that is present and invalid is still refused with a 401: only the
absence of one is anonymous.

## Authentication [#authentication]

```bash
curl -H "Authorization: Bearer psk_write_…" https://api.plansmith.co/v1/me
```

Two credential kinds work on the same header: a personal API key (`psk_…`) minted in
Settings → API keys, and an OAuth access token obtained by the CLI or an MCP client. Both act as the
human they belong to.

## Scopes [#scopes]

A credential is `read` or `write`. Scope is a ceiling on top of your own permissions, never a
widening of them: a write key held by someone with view-only access to a plan still cannot change
that plan.

## Errors [#errors]

```json
{
  "error": {
    "code": "not_found",
    "message": "No such plan."
  }
}
```

<Callout>
  A plan you cannot read answers 404, exactly like a plan that does not exist. That is deliberate: a
  403 would confirm which private plans are real, so the two are made indistinguishable. The one 403
  you will see on plan content is when you can read a plan but not edit it.
</Callout>

## How things are addressed [#how-things-are-addressed]

Projects have a slug, plans are `PLAN-n` within their project, and specs are `SPEC-n` within their
plan: the same handles you see in the web app, so a URL you read in the console is a path you can
call.

```text
/v1/projects/recipe-box
/v1/projects/recipe-box/plans/PLAN-12
/v1/projects/recipe-box/plans/PLAN-12/specs/SPEC-3
/v1/prototype-template                                # the workspace's, and its project twin
/v1/projects/recipe-box/prototype-template
```

## Images [#images]

```text
POST /v1/projects/recipe-box/plans/PLAN-12/images              # multipart
POST /v1/projects/recipe-box/plans/PLAN-12/images/upload-url   # for files over 4 MB
```

Send exactly one of a `file` part, a `url` (public https, fetched by the server), a `storage_ref`
from `upload-url`, or an `attachmentId` already on the plan. Add `spec=SPEC-3` or `epic=true` to
append the image to that document, and `alt` for its text. The response carries `attachmentId`,
`url`, the `markdown` to paste anywhere, `appendedTo` and the document's new `revision`.

Requests over about 4.5 MB never reach the API, so a larger file (up to 10 MB) goes in two steps:
`upload-url` returns a signed URL and a `storage_ref`; `PUT` the bytes to the URL, then post the
`storage_ref` with `filename` and `content_type` within 15 minutes.

An append is compare-and-set. Pass `expectedRevision` (the `revision` from your last read of the spec or
the epic) to make sure you add to the version you read. A `409` means the document changed and
nothing was added; its `details` carry `attachmentId` and `currentRevision`, so the retry reuses the
stored image rather than uploading it again. `PATCH …/specs/SPEC-3` and `PUT …/epic` take the same
`expectedRevision`. SVG and other formats are a `422`, a file over 10 MB a `413`.

## Prototypes [#prototypes]

A plan's prototype answers metadata by default (status, size, whether the specs changed since it
was built) and the HTML document with `html=1`. Add `standalone=1` to that for a page that opens
safely on its own in a browser: the prototype runs inside the same sandbox the web app uses.

```text
GET /v1/projects/recipe-box/plans/PLAN-12/prototype                       # metadata
GET /v1/projects/recipe-box/plans/PLAN-12/prototype?html=1                # the document
GET /v1/projects/recipe-box/plans/PLAN-12/prototype?html=1&standalone=1   # a page to open
```

A document over the size limit answers 413, and the metadata says so first with `tooLarge`. The
standalone page adds a little around the document, so one just under the limit can be refused
there and still download plain. `standalone=1` without `html=1` answers 400.

## Sharing [#sharing]

Sharing lives on the **top-level plan** and covers every sub-plan under it, so a call addressed at a
sub-plan changes the plan above it.

```text
GET    /v1/projects/recipe-box/plans/PLAN-12/sharing
PUT    /v1/projects/recipe-box/plans/PLAN-12/sharing/visibility
POST   /v1/projects/recipe-box/plans/PLAN-12/sharing/collaborators
PATCH  /v1/projects/recipe-box/plans/PLAN-12/sharing/collaborators/{userId}
DELETE /v1/projects/recipe-box/plans/PLAN-12/sharing/collaborators/{userId}
```

All four mutations land on the top-level plan, `PATCH` included: promoting someone from viewer to
editor grants them edit access across every sub-plan in the tree.

The GET carries `isSubPlan`, the root's name and `PLAN-n`, and `subPlanCount`: the ACTIVE
descendants, or `null` when the count could not be read. Read `isSubPlan` rather than comparing
`rootPlanId` to the handle you called with. One is a UUID and the other is a `PLAN-n`, so that
comparison reports every plan as a sub-plan.

Every successful mutation returns `scopeNote`, one sentence naming the plan the change landed on.
It is optional, not nullable: it is absent only when the read that composes it failed, which never
turns a committed change into an error.

`publicLinkCount` is a number or the string `"unknown"`. A failed read reports `"unknown"`, never
`0`, because `0` would claim an exclusivity it cannot prove.

## The full reference [#the-full-reference]

Every route, parameter and response shape is described by the OpenAPI document. It needs no
credential, and it is served from two addresses: the API host, and the site root, which is where a
client that only knows the domain will look.

```bash
curl https://api.plansmith.co/v1/openapi.json
curl https://www.plansmith.co/openapi.json
```

`https://www.plansmith.co/.well-known/api-catalog` lists all of it in RFC 9727 form: the OpenAPI
document, these guides, and the MCP endpoint.
