--- 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. ```bash curl -X POST "http://localhost:3100/api/companies" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Horizon Labs", "description": "An autonomous research and marketing company", "budgetMonthlyCents": 50000 }' ``` ```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(); ``` ```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() ``` --- ## 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. ```bash curl -X PATCH "http://localhost:3100/api/companies/company-1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Horizon Labs", "description": "Updated description", "budgetMonthlyCents": 75000, "status": "active" }' ``` ```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(); ``` ```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() ``` --- ## 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. ```bash curl -X POST "http://localhost:3100/api/companies/company-1/logo" \ -H "Authorization: Bearer " \ -F "file=@./logo.svg" ``` ```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(); ``` ```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() ``` 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.