mirror of
https://github.com/paperclipai/paperclip-docs.git
synced 2026-11-10 03:51:29 -10:00
Syncs docs to parent release **v2026.831.1** (cumulative from
`v2026.824.1`, via merge-base `664052f`). Authored fresh against the
release tag; every claim verified at the tag.
## What changed
**New**
- **Kimi Code adapter** (`kimi_local`) — new reference page, wired into
the adapters overview, nav, and anchor-map source mapping.
**Reference**
- **Env vars** — `PAPERCLIP_WORKSPACE_REAPER_COOLDOWN_DAYS`, opt-in
`SENTRY_DSN`, and the operator controls `PAPERCLIP_HIDDEN_SETTINGS` /
`PAPERCLIP_SETTING_DEFAULTS`.
- **Instance Settings** — new "operator controls for hosted deployments"
section (hide surfaces / override defaults).
- **Node.js floor → 24.11.0** across installation, setup,
local-development, plugin dev, Modal, and the getting-started/terminal
guides.
- **Grok adapter** — `permissionMode` no longer defaults to `dontAsk`;
when unset no flag is passed, and `--always-approve` is the unattended
policy.
- **CLI `agent`** — Kimi skills install path (`~/.kimi-code/skills`) +
`KIMI_CODE_HOME`.
**Guides**
- **Onboarding** — rebuilt around a single-card wizard that opens on
creating your agent; the mission step is gone, the goal is set
afterward.
- **Settings unification** — Invites folded into a Members tab, one
shared settings nav, and the company brand color + per-company
attachment size limit removed (also reflected in the Companies and Cases
APIs).
- **Governance behavior** — silent-run detection and stranded-task
recovery no longer act on their own; agent thought text is excluded from
automatic comments; invalid agent credentials now return `401`.
**Changelog** — entry added for v2026.831.1.
## Gates
- `docs:build`, SEO metadata, crawlable-links, sitemap-lastmod
(194/194), lint-links, verify-nav — all pass.
- **Edit verification:** every new high-confidence claim verified at the
tag.
- **Tag-accuracy gate:** 31 files checked, **0 post-tag leaks**, 30
clean, 1 review (below).
## ⚠ Drift (Verify before acting — not auto-resolved)
Medium-confidence REST-route drift in `docs/reference/api/companies.md`
— the import-transfer routes may no longer exist at these paths (likely
a false negative from prefix/dynamic registration; confirm before
editing):
- `POST /api/companies/import/transfers`
- `PUT /api/companies/import/transfers/{transferId}/parts/{partIndex}`
- `GET /api/companies/import/transfers/{transferId}`
- `POST /api/companies/import/transfers/{transferId}/preview`
- `POST /api/companies/import/transfers/{transferId}/apply`
## ⚠ Review
- `docs/reference/adapters/kimi-local.md` — new page; the only flagged
claim is the `secret-id` JSON **example placeholder** (not a real
identifier). Authored directly from the adapter's config surface at the
tag.
## Screenshots (deferred)
288 of 342 registered screenshots are stale (org-wide rename +
settings/onboarding rewrites), tracked in `SCREENSHOTS_PENDING.md`.
Recapture is a follow-up pass — **the merged pages describe the new
onboarding/settings UI while screenshots still show the old flow until
then.**
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
453 lines
16 KiB
Markdown
453 lines
16 KiB
Markdown
---
|
||
paperclip_version: v2026.831.1
|
||
seo_title: Companies API
|
||
seo_description: Companies are the tenant boundary every agent, project, issue, and cost event belongs to. Endpoints for managing them, and how scoping is enforced.
|
||
---
|
||
|
||
# Companies
|
||
|
||
Companies are the top-level tenant boundary in Paperclip. Every agent, project, issue, approval, cost event, and asset belongs to exactly one company, and the API enforces that boundary on every company-scoped route.
|
||
|
||
Board callers can list and manage companies. Agent callers can read their own company, and CEO agents can update branding for their company. Deleting a company is destructive; archiving is not.
|
||
|
||
---
|
||
|
||
## Company shape
|
||
|
||
The company API returns a compact company object with the fields you will use most often:
|
||
|
||
| Field | Meaning |
|
||
|---|---|
|
||
| `id` | Stable company id |
|
||
| `name` | Display name |
|
||
| `description` | Optional plain-text description |
|
||
| `status` | `active`, `paused`, or `archived` |
|
||
| `issuePrefix` | Auto-generated issue prefix, based on the company name |
|
||
| `issueCounter` | Next issue number for the company |
|
||
| `budgetMonthlyCents` | Company budget ceiling in cents |
|
||
| `spentMonthlyCents` | Current month spend in cents |
|
||
| `requireBoardApprovalForNewAgents` | Whether hires need board approval |
|
||
| `logoAssetId` | Stored logo asset id, or `null` |
|
||
| `logoUrl` | Derived content URL for the stored logo, or `null` |
|
||
| `createdAt` | Creation timestamp |
|
||
| `updatedAt` | Last update timestamp |
|
||
|
||
The response also includes feedback-sharing consent fields when they are set. `logoUrl` is derived from `logoAssetId`; you do not set it directly.
|
||
|
||
---
|
||
|
||
## List companies
|
||
|
||
```
|
||
GET /api/companies
|
||
```
|
||
|
||
Returns the companies the current board caller can access. In normal use this is your company list, not every company in the instance.
|
||
|
||
> **Note:** This is a board route. Agent callers do not use it.
|
||
|
||
## Get a company
|
||
|
||
```
|
||
GET /api/companies/{companyId}
|
||
```
|
||
|
||
Returns one company object if the caller has access to that company.
|
||
|
||
---
|
||
|
||
## List company artifacts
|
||
|
||
```
|
||
GET /api/companies/{companyId}/artifacts
|
||
```
|
||
|
||
Returns a company-wide projection of the outputs agents produced while working. It flattens three sources — agent-authored issue documents, direct issue attachments, and `artifact` work products — into one card-ready list, so callers do not have to stitch per-issue endpoints together. Company access is checked before the projection runs.
|
||
|
||
### Query parameters
|
||
|
||
| Parameter | Values | Description |
|
||
|---|---|---|
|
||
| `kind` | `image`, `video`, `text`, `document`, `file`, `all` | Filter by media kind. Defaults to `all`. |
|
||
| `projectId` | UUID | Restrict to artifacts from issues in one project. |
|
||
| `q` | string (max 160 chars) | Free-text search over artifact title/summary and originating issue identifier/title. |
|
||
| `groupBy` | `none`, `task`, `parent_task` | Grouping mode. `none` (default) returns a flat artifact list; `task` groups by the originating issue; `parent_task` rolls sub-issue artifacts up under their root issue. |
|
||
| `groupIssueId` | UUID | When grouping, expand a single group (stack) into its own artifacts. |
|
||
| `starred` | `true`, `false` | Return only the documents the calling user has starred. Defaults to `false`. |
|
||
| `limit` | integer 1–100 | Page size. Defaults to `30`. |
|
||
| `cursor` | string | Opaque pagination cursor from a prior response's `nextCursor`. |
|
||
|
||
### Your starred documents
|
||
|
||
Set `starred=true` when you want the shortlist instead of everything — the documents you personally starred, newest star first. A few things follow from that:
|
||
|
||
- Only documents come back. Work products and attachments are left out, because stars only exist for documents. Pairing `starred=true` with a `kind` other than `all` or `document` therefore returns nothing.
|
||
- The usual document filters are relaxed. A starred document is included even if no agent authored or edited it, and even if it is one of the system issue documents that the unfiltered list hides.
|
||
- Stars are per-user, so this only works for a signed-in board caller. An agent caller asking for `starred=true` gets an empty list rather than someone else's stars.
|
||
- `cursor` paging still works, and pages by when you starred each document.
|
||
|
||
To add or remove a star, use the document route on the [Resource Memberships](./resource-memberships.md) page.
|
||
|
||
### Response
|
||
|
||
The response is an envelope. In the ungrouped case it returns the flat `artifacts` list; when `groupBy` is `task` or `parent_task` it returns `groups` instead (or, when `groupIssueId` is supplied, the expanded group's `artifacts` plus `selectedGroup`). `nextCursor` is `null` when there are no further pages.
|
||
|
||
```json
|
||
{
|
||
"artifacts": [
|
||
{
|
||
"id": "work_product:...",
|
||
"source": "work_product",
|
||
"mediaKind": "video",
|
||
"title": "Launch teaser render",
|
||
"previewText": null,
|
||
"contentType": "video/mp4",
|
||
"contentPath": "/api/attachments/.../content",
|
||
"openPath": "/api/attachments/.../content",
|
||
"downloadPath": "/api/attachments/.../content?download=1",
|
||
"issue": { "id": "...", "identifier": "PAP-123", "title": "Produce launch teaser" },
|
||
"project": { "id": "...", "name": "Launch" },
|
||
"createdByAgent": { "id": "...", "name": "Video Agent" },
|
||
"updatedAt": "2026-06-01T12:00:00.000Z",
|
||
"href": "/PAP/issues/PAP-123#work-product-..."
|
||
}
|
||
],
|
||
"nextCursor": null
|
||
}
|
||
```
|
||
|
||
Each artifact carries:
|
||
|
||
| Field | Meaning |
|
||
|---|---|
|
||
| `id` | Stable artifact id, prefixed by source (`document:`, `attachment:`, or `work_product:`) |
|
||
| `source` | `document`, `attachment`, or `work_product` |
|
||
| `mediaKind` | `image`, `video`, `text`, `document`, `file`, or `empty` |
|
||
| `title` | Display title |
|
||
| `previewText` | Short plain-text preview, or `null` |
|
||
| `contentType` | MIME type of the underlying content, or `null` |
|
||
| `contentPath` | Path to the raw content, or `null` |
|
||
| `openPath` | Path for inline open, or `null` |
|
||
| `downloadPath` | Path for download, or `null` |
|
||
| `issue` | `{ id, identifier, title }` of the originating issue |
|
||
| `project` | `{ id, name }` of the issue's project, or `null` |
|
||
| `createdByAgent` | `{ id, name }` of the producing agent, or `null` |
|
||
| `updatedAt` | Last-updated timestamp |
|
||
| `href` | Deep link to the artifact on its originating issue |
|
||
|
||
A group object (returned under `groups` / `selectedGroup`) carries `id`, `groupBy`, `issue`, `title`, `count`, `mediaKinds`, `previewArtifacts`, `updatedAt`, and `href`.
|
||
|
||
---
|
||
|
||
## Create a company
|
||
|
||
```
|
||
POST /api/companies
|
||
{
|
||
"name": "Horizon Labs",
|
||
"description": "An autonomous research and marketing company",
|
||
"budgetMonthlyCents": 50000
|
||
}
|
||
```
|
||
|
||
Creates a new company and assigns it an automatic issue prefix. If `budgetMonthlyCents` is greater than `0`, Paperclip also creates the matching monthly budget policy in UTC calendar-month mode.
|
||
|
||
This route is board-facing. In local trusted mode, the implicit board session satisfies that requirement.
|
||
|
||
<!-- tabs: cURL, JavaScript, Python -->
|
||
|
||
<!-- tab: cURL -->
|
||
```bash
|
||
curl -X POST "http://localhost:3100/api/companies" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"name": "Horizon Labs",
|
||
"description": "An autonomous research and marketing company",
|
||
"budgetMonthlyCents": 50000
|
||
}'
|
||
```
|
||
<!-- tab: JavaScript -->
|
||
```js
|
||
const res = await fetch("http://localhost:3100/api/companies", {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
name: "Horizon Labs",
|
||
description: "An autonomous research and marketing company",
|
||
budgetMonthlyCents: 50000,
|
||
}),
|
||
});
|
||
const company = await res.json();
|
||
```
|
||
<!-- tab: Python -->
|
||
```py
|
||
import requests
|
||
|
||
res = requests.post(
|
||
"http://localhost:3100/api/companies",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
json={
|
||
"name": "Horizon Labs",
|
||
"description": "An autonomous research and marketing company",
|
||
"budgetMonthlyCents": 50000,
|
||
},
|
||
)
|
||
company = res.json()
|
||
```
|
||
<!-- /tabs -->
|
||
|
||
---
|
||
|
||
## Update a company
|
||
|
||
```
|
||
PATCH /api/companies/{companyId}
|
||
{
|
||
"name": "Horizon Labs",
|
||
"description": "Updated description",
|
||
"budgetMonthlyCents": 75000,
|
||
"status": "active",
|
||
"logoAssetId": "11111111-1111-4111-8111-111111111111"
|
||
}
|
||
```
|
||
|
||
Board callers can update the full company record here. CEO agents can also call this route, but only with branding fields. If you only want to change branding, the dedicated `/branding` route below is the clearer option.
|
||
|
||
Important branding caveat:
|
||
|
||
- `logoAssetId` must point to an asset that belongs to the same company.
|
||
- Replacing or clearing the logo removes the previous logo asset record.
|
||
- `logoUrl` is derived by the server after the logo is linked.
|
||
|
||
<!-- tabs: cURL, JavaScript, Python -->
|
||
|
||
<!-- tab: cURL -->
|
||
```bash
|
||
curl -X PATCH "http://localhost:3100/api/companies/company-1" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"name": "Horizon Labs",
|
||
"description": "Updated description",
|
||
"budgetMonthlyCents": 75000,
|
||
"status": "active"
|
||
}'
|
||
```
|
||
<!-- tab: JavaScript -->
|
||
```js
|
||
const res = await fetch("http://localhost:3100/api/companies/company-1", {
|
||
method: "PATCH",
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
name: "Horizon Labs",
|
||
description: "Updated description",
|
||
budgetMonthlyCents: 75000,
|
||
status: "active",
|
||
}),
|
||
});
|
||
const company = await res.json();
|
||
```
|
||
<!-- tab: Python -->
|
||
```py
|
||
import requests
|
||
|
||
res = requests.patch(
|
||
"http://localhost:3100/api/companies/company-1",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
json={
|
||
"name": "Horizon Labs",
|
||
"description": "Updated description",
|
||
"budgetMonthlyCents": 75000,
|
||
"status": "active",
|
||
},
|
||
)
|
||
company = res.json()
|
||
```
|
||
<!-- /tabs -->
|
||
|
||
---
|
||
|
||
## Update branding
|
||
|
||
```
|
||
PATCH /api/companies/{companyId}/branding
|
||
{
|
||
"name": "Horizon Labs",
|
||
"logoAssetId": "11111111-1111-4111-8111-111111111111"
|
||
}
|
||
```
|
||
|
||
Use this when you want a narrower branding-only update. The route accepts:
|
||
|
||
- `name`
|
||
- `description`
|
||
- `logoAssetId`
|
||
|
||
Board callers can use it. CEO agents can use it only for their own company.
|
||
|
||
If you pass a `logoAssetId` that belongs to a different company, the API returns `422`.
|
||
|
||
---
|
||
|
||
## Upload a company logo
|
||
|
||
```
|
||
POST /api/companies/{companyId}/logo
|
||
Content-Type: multipart/form-data
|
||
```
|
||
|
||
This route uploads the logo image itself and returns an asset record. It does **not** attach the logo to the company automatically. After the upload succeeds, send the returned `assetId` to `PATCH /api/companies/{companyId}` or `PATCH /api/companies/{companyId}/branding` as `logoAssetId`.
|
||
|
||
Accepted image types:
|
||
|
||
- `image/png`
|
||
- `image/jpeg`
|
||
- `image/jpg`
|
||
- `image/webp`
|
||
- `image/gif`
|
||
- `image/svg+xml`
|
||
|
||
Logo uploads use the normal attachment size limit. SVG files are sanitized before storage. Scripts and external links are stripped, and empty or unsafe SVGs are rejected.
|
||
|
||
<!-- tabs: cURL, JavaScript, Python -->
|
||
|
||
<!-- tab: cURL -->
|
||
```bash
|
||
curl -X POST "http://localhost:3100/api/companies/company-1/logo" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-F "file=@./logo.svg"
|
||
```
|
||
<!-- tab: JavaScript -->
|
||
```js
|
||
const formData = new FormData();
|
||
formData.append("file", fileInput.files[0]);
|
||
|
||
const res = await fetch("http://localhost:3100/api/companies/company-1/logo", {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: formData,
|
||
});
|
||
const result = await res.json();
|
||
```
|
||
<!-- tab: Python -->
|
||
```py
|
||
import requests
|
||
|
||
with open("logo.svg", "rb") as f:
|
||
res = requests.post(
|
||
"http://localhost:3100/api/companies/company-1/logo",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
files={"file": ("logo.svg", f, "image/svg+xml")},
|
||
)
|
||
|
||
result = res.json()
|
||
```
|
||
<!-- /tabs -->
|
||
|
||
The upload response includes `assetId` and `contentPath`. `contentPath` points at `/api/assets/{assetId}/content`.
|
||
|
||
---
|
||
|
||
## Company stats
|
||
|
||
```
|
||
GET /api/companies/stats
|
||
```
|
||
|
||
Returns a simple summary keyed by company id:
|
||
|
||
```json
|
||
{
|
||
"company-1": { "agentCount": 5, "issueCount": 42 }
|
||
}
|
||
```
|
||
|
||
This route is board-only.
|
||
|
||
---
|
||
|
||
## Feedback traces
|
||
|
||
```
|
||
GET /api/companies/{companyId}/feedback-traces
|
||
```
|
||
|
||
Board-only route for reviewing feedback activity across a company. Query filters include:
|
||
|
||
- `targetType`
|
||
- `vote`
|
||
- `status`
|
||
- `issueId`
|
||
- `projectId`
|
||
- `from`
|
||
- `to`
|
||
- `sharedOnly`
|
||
- `includePayload`
|
||
|
||
Use this when you want to inspect the feedback loop, not normal company activity.
|
||
|
||
---
|
||
|
||
## Export and import
|
||
|
||
Paperclip exposes both broad and company-scoped portability routes:
|
||
|
||
| Route | Purpose | Who can call it |
|
||
|---|---|---|
|
||
| `POST /api/companies/{companyId}/export` | Export a company bundle | Any caller with access to the company |
|
||
| `POST /api/companies/{companyId}/exports/preview` | Preview a company export | Board or CEO of that company |
|
||
| `POST /api/companies/{companyId}/exports` | Export a company bundle with the stricter portability flow | Board or CEO of that company |
|
||
| `POST /api/companies/import/preview` | Preview an import into a new or existing company | Board only |
|
||
| `POST /api/companies/{companyId}/imports/preview` | Preview an import into the same company | Board or CEO of that company |
|
||
| `POST /api/companies/{companyId}/imports/apply` | Apply an import into the same company | Board or CEO of that company |
|
||
|
||
The company-scoped `imports/*` routes are intentionally safer:
|
||
|
||
- they can only target the route company
|
||
- they reject the `replace` collision strategy
|
||
|
||
Use the board-level preview route when you need to inspect an import that would create a brand-new company or target a different company.
|
||
|
||
### Chunked import transfers
|
||
|
||
A large export doesn't have to arrive in one upload. When a package is big, the client slices it into parts and streams them one at a time through a **transfer** — a durable run the server tracks so an interrupted upload resumes from the parts it already has instead of starting over. The UI and `paperclipai company import` both use this path automatically; see the [Export & Import guide](../../guides/power/export-import.md) for the user-facing flow.
|
||
|
||
| Route | Purpose | Who can call it |
|
||
|---|---|---|
|
||
| `POST /api/companies/import/transfers` | Start — or resume — a chunked import transfer and get its `transferId` | Board only |
|
||
| `PUT /api/companies/import/transfers/{transferId}/parts/{partIndex}` | Upload one part of the package | Board only |
|
||
| `GET /api/companies/import/transfers/{transferId}` | Check the transfer's progress and which parts are still missing | Board only |
|
||
| `POST /api/companies/import/transfers/{transferId}/preview` | Preview the reassembled package before applying it | Board only |
|
||
| `POST /api/companies/import/transfers/{transferId}/apply` | Reassemble the parts, verify the whole-file hash, and run the import | Board only |
|
||
|
||
Declaring a transfer is idempotent: the manifest carries a content-derived key, so re-declaring the same package returns the same run rather than starting a fresh one. Once a transfer completes, that exact package is done — a repeat declare short-circuits and you re-export the company to import it again.
|
||
|
||
---
|
||
|
||
## Archive or delete
|
||
|
||
```
|
||
POST /api/companies/{companyId}/archive
|
||
DELETE /api/companies/{companyId}
|
||
```
|
||
|
||
Archiving changes the company status to `archived` and removes it from default listings. It also runs a cascade that pauses every active agent in the company (pause reason `company_archived`) and cancels their queued or in-flight wake-up requests, so an archived company stops doing background work. Deleting removes the company and its related data. Deletion is destructive and should be treated as irreversible.
|
||
|
||
---
|
||
|
||
## Practical notes
|
||
|
||
- Company access is always checked before reading or mutating company data.
|
||
- `logoUrl` is derived from the stored logo asset and should be treated as read-only.
|
||
- If you are updating branding only, prefer `PATCH /api/companies/{companyId}/branding`.
|
||
- If you are uploading a logo, remember the upload and the company update are two separate steps.
|