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 |
brandColor |
Optional hex brand color |
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. |
limit |
integer 1–100 | Page size. Defaults to 30. |
cursor |
string | Opaque pagination cursor from a prior response's nextCursor. |
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.
{
"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.
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
}'
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();
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()
Update a company
PATCH /api/companies/{companyId}
{
"name": "Horizon Labs",
"description": "Updated description",
"budgetMonthlyCents": 75000,
"status": "active",
"brandColor": "#2563eb",
"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:
logoAssetIdmust point to an asset that belongs to the same company.- Replacing or clearing the logo removes the previous logo asset record.
logoUrlis derived by the server after the logo is linked.
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",
"brandColor": "#2563eb"
}'
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",
brandColor: "#2563eb",
}),
});
const company = await res.json();
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",
"brandColor": "#2563eb",
},
)
company = res.json()
Update branding
PATCH /api/companies/{companyId}/branding
{
"name": "Horizon Labs",
"brandColor": "#2563eb",
"logoAssetId": "11111111-1111-4111-8111-111111111111"
}
Use this when you want a narrower branding-only update. The route accepts:
namedescriptionbrandColorlogoAssetId
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/pngimage/jpegimage/jpgimage/webpimage/gifimage/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.
curl -X POST "http://localhost:3100/api/companies/company-1/logo" \
-H "Authorization: Bearer <token>" \
-F "file=@./logo.svg"
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();
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()
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:
{
"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:
targetTypevotestatusissueIdprojectIdfromtosharedOnlyincludePayload
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
replacecollision 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.
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.
logoUrlis 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.