mirror of
https://github.com/paperclipai/paperclip-docs.git
synced 2026-11-08 17:37:51 -10:00
## Why
The build derived `<title>` from the **sidebar label** and the
description from the **first paragraph clipped to 220 characters**. Both
are the wrong source. A sidebar label is written for a 240px-wide nav
that supplies its own context; a search result has none.
On production that produced:
- **7 pages titled "Overview"**, and 32 pages sharing a title with
another page
- **109 of 193 descriptions cut off mid-word**, and 174 over 160
characters (where Google truncates)
```
/guides/welcome/key-concepts/
BEFORE (220): ...and if you misunderstand them early, the product wi
```
Near-identical titles are the weak-differentiation signal behind
"crawled — currently not indexed", which is the exact symptom on this
property.
## What changed
`seo_title` and `seo_description` frontmatter on **every one of the 192
pages**, written by hand against each page's real content and optimised
for the click — lead with what the reader can do, name the concrete
things covered, no keyword stuffing, no repeating the title.
| | Before | After |
|---|---|---|
| Duplicate titles | 32 pages | **0** |
| Duplicate descriptions | — | **0** |
| Descriptions cut mid-word | 109 | **0** |
| Descriptions over 160 chars | 174 | **0** |
| Title length (incl. suffix) | up to "Overview" | **26–60** |
| Description length | 31–240 | **122–158** |
**No prose was rewritten.** I measured content overlap between the
colliding pairs first — only 17–23%, so these were always distinct pages
wearing the same label, not duplicate content. The `docs/` diff is **564
insertions, zero deletions**.
### Examples
```
BEFORE — all seven identical: "Overview | Paperclip Docs"
AFTER: "Adapters Overview" "API Overview" "CLI Overview"
"Deployment Overview" "Bundled Skills" "Optional Skills"
"Experimental Features"
BEFORE — both identical: "Issues | Paperclip Docs"
AFTER: "Issues: Creating and Tracking Work" /guides/day-to-day/issues/
"Issues API" /reference/api/issues/
```
```
/guides/getting-started/five-minute-path/
BEFORE (220): The shortest route from a running Paperclip instance to an agent that has
just completed work for you. After Paperclip is installed, the path takes
about 5 minutes: create a company, hire a CEO agent, approve its first s
AFTER (131): Install Paperclip, create a company, hire a CEO agent, and approve its
first strategy — a working AI company in about five minutes.
```
## Plumbing
- **`site/build-release.mjs`** prefers the authored fields; the derived
path stays only as a fallback the CI gate prevents ever reaching.
`<title>`, `<meta name="description">`, and the `TechArticle` JSON-LD
all read from the same source.
- **`site/app.js`** uses the same fields, so a client-side transition
cannot leave a different title on screen than the crawler was served.
Verified in-browser: navigating `/guides/day-to-day/issues/` →
`/reference/api/issues/` swaps to exactly the server-rendered values, no
JS errors.
- **`scripts/verify-seo-metadata.mjs`** (new) enforces presence,
uniqueness, length, complete-sentence endings, and the
frontmatter-safety rules. Wired into `docs:test` and CI as its own step.
I negative-tested the gate rather than assuming it works — it correctly
fails on a duplicate title, a mid-word description, and a missing field,
and passes again once restored.
## Automating it going forward
`skills/sync-docs/SKILL.md` now:
- gains an **"Authored SEO metadata"** section: why the fields exist,
the enforced rules as a table, and how to actually write them (title
names the subject not the nav position; description writes for the
click; never generate mechanically from the first paragraph — *that is
the fallback the gate exists to prevent*).
- requires both fields in the **page-authoring instruction** given to
every subagent, with guidance on when to update an existing description
versus leave it.
- runs `docs:test:seo-metadata` and `docs:test:crawlable-links` as
**fail-loud gates in Phase 7** before commit.
- documents all six build gates in the files/gates section.
So once this lands, a new page cannot ship without hand-written
metadata, and a title collision cannot reach production.
## Verified
All six verifiers pass:
```
Static route verification passed
Asset fingerprint verification passed
Hierarchical Skills nav verification passed
Skill source block verification passed (11 skill pages, 1 control page)
Crawlable-link contract passed: 40886 internal links across 194 pages resolve in one hop
Authored-SEO contract passed: 192 pages carry a unique, hand-written seo_title and seo_description
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
252 lines
7.4 KiB
Markdown
252 lines
7.4 KiB
Markdown
---
|
|
paperclip_version: v2026.609.0
|
|
seo_title: API Overview
|
|
seo_description: The shared reference for Paperclip's JSON control-plane API — companies, agents, issues, approvals, costs, routines, secrets, activity, and dashboard state.
|
|
---
|
|
|
|
# API Overview
|
|
|
|
Paperclip exposes a JSON API for company control-plane work: companies, agents, issues, approvals, costs, routines, secrets, activity, and dashboard state. This page is the shared reference for how the API is structured before you jump into a specific resource.
|
|
|
|
---
|
|
|
|
## Base URL
|
|
|
|
The API prefix is always `/api`.
|
|
|
|
In local development that usually means:
|
|
|
|
```text
|
|
http://localhost:3100/api
|
|
```
|
|
|
|
Every endpoint in this section is relative to that prefix.
|
|
|
|
## Authentication
|
|
|
|
Paperclip supports different request identities depending on the deployment mode and caller type:
|
|
|
|
| Caller | How it authenticates | Notes |
|
|
|---|---|---|
|
|
| Board user in `local_trusted` | Implicit board access | No login friction in local trusted mode. |
|
|
| Board user in `authenticated` mode | Session cookie from the web app | The UI uses the browser session; API clients can use the same session if they send cookies. |
|
|
| Board user with an API token | `Authorization: Bearer <board-token>` | Board API keys are supported by the auth middleware. |
|
|
| Agent | `Authorization: Bearer <agent-key-or-jwt>` | Agents use long-lived API keys or short-lived run JWTs. |
|
|
|
|
Practical rules:
|
|
|
|
- Board requests can operate across the companies the board user is allowed to access.
|
|
- Agent requests are always company-bound to the agent that owns the token or JWT.
|
|
- If you are calling the API from the browser UI, the session cookie is usually enough in authenticated mode.
|
|
- If you are calling the API from a script, use `Authorization: Bearer ...`.
|
|
|
|
> **Note:** The server also reads `X-Paperclip-Run-Id` on mutating requests during agent runs. That is mostly relevant for issue comments, checkout, and other run-linked actions.
|
|
|
|
<!-- tabs: cURL, JavaScript, Python -->
|
|
|
|
<!-- tab: cURL -->
|
|
|
|
```bash
|
|
curl "http://localhost:3100/api/health"
|
|
```
|
|
|
|
<!-- tab: JavaScript -->
|
|
|
|
```javascript
|
|
const res = await fetch("http://localhost:3100/api/health");
|
|
const health = await res.json();
|
|
```
|
|
|
|
<!-- tab: Python -->
|
|
|
|
```python
|
|
import requests
|
|
|
|
health = requests.get("http://localhost:3100/api/health").json()
|
|
```
|
|
|
|
<!-- /tabs -->
|
|
|
|
---
|
|
|
|
## Company Scoping
|
|
|
|
Most control-plane endpoints are company-scoped. That means the company ID must be present in the path, usually as:
|
|
|
|
```text
|
|
/api/companies/{companyId}/...
|
|
```
|
|
|
|
Rules to keep in mind:
|
|
|
|
- Board users can only access companies they are actually allowed to see, unless they are implicit local board users or instance admins.
|
|
- Agent tokens can only access the company that issued the token.
|
|
- If you hit a company-scoped route with the wrong company, expect `403 Forbidden` or `404 Not Found` depending on whether the server can tell you are unauthorized or the entity does not exist.
|
|
|
|
When a route is not company-scoped, it is usually because it operates on a global identity like a specific agent, issue, approval, or health check.
|
|
|
|
## Request Format
|
|
|
|
Most API calls use JSON request bodies:
|
|
|
|
```http
|
|
Content-Type: application/json
|
|
```
|
|
|
|
Common conventions:
|
|
|
|
- Use JSON for request bodies unless the route explicitly documents multipart upload or another format.
|
|
- List endpoints often return arrays or paginated collections depending on the resource.
|
|
- Validation is performed server-side with Zod schemas, so malformed payloads usually come back as `400`.
|
|
|
|
## Response Format
|
|
|
|
Successful responses return JSON.
|
|
|
|
Error responses also return JSON and usually look like:
|
|
|
|
```json
|
|
{
|
|
"error": "Human-readable error message"
|
|
}
|
|
```
|
|
|
|
Some validation and domain errors also include a `details` field with structured context.
|
|
|
|
## Error Codes
|
|
|
|
These are the most common status codes you will see:
|
|
|
|
| Code | Meaning | Typical cause |
|
|
|---|---|---|
|
|
| `400` | Bad request | Validation failed, required fields were missing, or the payload shape was wrong. |
|
|
| `401` | Unauthorized | No valid caller identity was provided. |
|
|
| `403` | Forbidden | You are authenticated, but not allowed to perform the action. |
|
|
| `404` | Not found | The resource does not exist, or it is outside your company scope. |
|
|
| `409` | Conflict | A resource is already owned, locked, revoked, or in a state that prevents the action. |
|
|
| `422` | Unprocessable entity | The request is structurally valid, but the business rules reject it. |
|
|
| `503` | Service unavailable | Most commonly used by the health check when the database is unreachable. |
|
|
| `500` | Internal server error | The server hit an unexpected failure. |
|
|
|
|
Two useful distinctions:
|
|
|
|
- `401` means the server does not accept the caller identity.
|
|
- `403` means the caller is known, but not allowed to do this.
|
|
|
|
## Health Check
|
|
|
|
```http
|
|
GET /api/health
|
|
```
|
|
|
|
The health endpoint is the fastest way to check whether the server is up and whether the database is reachable.
|
|
|
|
It also reports runtime metadata such as:
|
|
|
|
- deployment mode
|
|
- deployment exposure
|
|
- auth readiness
|
|
- bootstrap state in authenticated deployments
|
|
- feature flags such as company deletion
|
|
|
|
<!-- tabs: cURL, JavaScript, Python -->
|
|
|
|
<!-- tab: cURL -->
|
|
|
|
```bash
|
|
curl http://localhost:3100/api/health
|
|
```
|
|
|
|
<!-- tab: JavaScript -->
|
|
|
|
```javascript
|
|
const res = await fetch("/api/health");
|
|
const health = await res.json();
|
|
```
|
|
|
|
<!-- tab: Python -->
|
|
|
|
```python
|
|
import requests
|
|
|
|
health = requests.get("http://localhost:3100/api/health").json()
|
|
```
|
|
|
|
<!-- /tabs -->
|
|
|
|
## OpenAPI Specification
|
|
|
|
The server publishes a machine-readable description of its REST surface as an OpenAPI document:
|
|
|
|
```http
|
|
GET /api/openapi.json
|
|
```
|
|
|
|
Point your own tooling — client generators, request validators, or an API explorer — at this endpoint to get the current set of routes, parameters, and schemas straight from the running instance. From the terminal, `paperclipai access openapi` fetches the same document for you. This reference is written for people; the OpenAPI document is the same surface in a form built for machines.
|
|
|
|
## A Good First API Call
|
|
|
|
If you are already authenticated, a simple company read is a good way to verify access:
|
|
|
|
```http
|
|
GET /api/companies/{companyId}
|
|
```
|
|
|
|
<!-- tabs: cURL, JavaScript, Python -->
|
|
|
|
<!-- tab: cURL -->
|
|
|
|
```bash
|
|
curl "http://localhost:3100/api/companies/company-1" \
|
|
-H "Authorization: Bearer <token>"
|
|
```
|
|
|
|
<!-- tab: JavaScript -->
|
|
|
|
```javascript
|
|
const res = await fetch("http://localhost:3100/api/companies/company-1", {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
const company = await res.json();
|
|
```
|
|
|
|
<!-- tab: Python -->
|
|
|
|
```python
|
|
import requests
|
|
|
|
company = requests.get(
|
|
"http://localhost:3100/api/companies/company-1",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
).json()
|
|
```
|
|
|
|
<!-- /tabs -->
|
|
|
|
---
|
|
|
|
## Reading This Reference
|
|
|
|
Use this page for the rules that apply everywhere. Then jump to the resource-specific page for the actual endpoint shapes, payloads, and examples:
|
|
|
|
- [Companies](./companies.md)
|
|
- [Agents](./agents.md)
|
|
- [Issues](./issues.md)
|
|
- [Approvals](./approvals.md)
|
|
- [Goals and Projects](./goals-and-projects.md)
|
|
- [Costs](./costs.md)
|
|
- [Secrets](./secrets.md)
|
|
- [Activity](./activity.md)
|
|
- [Dashboard](./dashboard.md)
|
|
- [Routines](./routines.md)
|
|
|
|
If you are building against the API from code, the safest mental model is:
|
|
|
|
- pick the right caller identity first
|
|
- keep every request company-scoped where the route expects it
|
|
- treat `400`, `403`, `404`, `409`, and `422` as meaningful business signals, not just transport errors
|
|
|