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.
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 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.

{
  "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:

  • 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.
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:

  • name
  • description
  • brandColor
  • 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.


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.

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:

  • 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 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.