Files
paperclip-docs/docs/reference/api/overview.md
Aron PrinsandClaude Opus 5 325c9eb819 seo: hand-write titles and descriptions for all 192 pages (#115)
## 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]>
2026-08-25 22:51:42 +02:00

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