Plansmith API
A versioned JSON API at api.plansmith.co/v1, authenticated with a bearer credential.
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.
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.
curl https://api.plansmith.co/v1Send 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
curl -H "Authorization: Bearer psk_write_…" https://api.plansmith.co/v1/meTwo 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
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
{
"error": {
"code": "not_found",
"message": "No such plan."
}
}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.
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.
/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-templateImages
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 MBSend 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
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.
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 openA 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 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.
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
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.
curl https://api.plansmith.co/v1/openapi.json
curl https://www.plansmith.co/openapi.jsonhttps://www.plansmith.co/.well-known/api-catalog lists all of it in RFC 9727 form: the OpenAPI
document, these guides, and the MCP endpoint.