openapi: 3.1.0 info: title: Origami Agent API version: "2.0" description: | The Origami **v2 API** is the single canonical public surface, organized into five segments — **Projects** (tenancy), **Agents**, **Workspace**, **Outreach** (campaigns, sequences, steps), and **Account**. Drive AI workers from a prompt, read and write table data (rows, cells, upsert, enrichment runs), manage documents and workspaces, control the sequencer and campaigns, run recurring scheduled agents, and read account/credit state. **Objects are self-describing** (Stripe-style): every object in a response carries an `object` field naming its type (`"agent"`, `"run"`, `"table"`, `"campaign"`, ...), and every list endpoint returns the one list envelope `{ "object": "list", "items": [...], "nextCursor": string|null, "url": string }`. **v1 is deprecated** (still fully functional — no removal date in this release). New integrations should target v2 only. See the [v1 → v2 migration guide](./api-v1-to-v2-migration) for the route map; in short: | v1 | v2 | | --- | --- | | `GET /tables` | `GET /api/v2/tables` | | `GET /tables/:id/rows` | `GET /api/v2/tables/:tableId/rows` | | `POST /tables/:id/rows` (insert) | `POST /api/v2/tables/:tableId/rows/upsert` (upsert; no bare insert) | | `GET /batches/:id` | `GET /api/v2/enrichment-runs/:runId` (`GET /api/v2/batches/:batchId` remains a deprecated alias) | | `GET /credits` | `GET /api/v2/account/credits` | Errors use the canonical envelope `{ error, code, details?, handoff? }`; any 4xx the user can fix in-app carries a forwardable `handoff` link. ## Tenancy — parent-wide keys & the `x-origami-project` header Every API key is **parent-wide**: bound to the parent (agency) organization and able to act on the parent and any of its **projects** (child orgs). Send the **`x-origami-project: `** header on any v2 request to scope it to that project — all org-scoped resources (workspaces, tables, agents, outreach) then operate inside the project. Omit the header to act on the parent. Two exceptions: `/projects/*` manages the projects themselves from the parent (the header is ignored there, never rejected), and `/account` is always parent-scoped. The header fails closed: a malformed id → `400 VALIDATION_ERROR`; an unknown, cross-parent, or deleted project → `404 PROJECT_NOT_FOUND`. The plan gate (`canUseApi`) and request-rate limits stay keyed to the parent, and the concurrent-agent pool is shared across the whole parent. Credits spent inside a project draw on the parent's shared wallet, subject to the project's optional `monthlyCredits` budget cap. ## Transport — async by default `POST /agents` and `POST /agents/:id/runs` respond `202 Accepted` with the initial run object (`status: "running"`) as soon as the run is admitted. The actual agent work runs in the background. Callers poll `GET /agents/:id/runs/:runId` until `status !== "running"` to read the terminal state. Every endpoint returns plain `application/json`. There is no NDJSON, no heartbeats, no `tail -1`, no pre-stream vs in-stream error branches. Closing the HTTP socket has no effect on the run — there is no `abortOnDisconnect` knob. SDKs hide the polling — `await origami.agents.create({ prompt })` returns the final terminal `Run` object. Direct REST consumers poll `GET /agents/:id/runs/:runId` and honor the `Retry-After` header it returns while the run is still `running` (currently `15` seconds). The header is omitted on terminal responses — that absence is the signal to stop polling. Runs typically take 1–5 minutes; polling faster than the hint does not make a run finish sooner. ## Pagination — cursor only Every list endpoint takes the same parameter pair — `limit` (default 50, max 100; row queries allow up to 200) and `cursor` (opaque; pass back the previous page's `nextCursor`) — and returns the one envelope `{ object: "list", items, nextCursor, url }`. `nextCursor: null` marks the last page. There is no `page`/`pageSize` and no `offset`. Per-endpoint filters (e.g. `?search=`, `?status=`, `?enabled=`) compose with `cursor`/`limit`. ## Idempotency Any `POST` may send an **`Idempotency-Key`** header (1–255 printable ASCII chars). The first response for a key is recorded and replayed verbatim for retries with the same key + request for 24 hours; replayed responses carry the `Idempotency-Replay: true` header. Reusing a key with a *different* method/path/body — or racing a still-in-flight original — is `409 IDEMPOTENCY_ERROR`. Row upserts additionally dedup on the body `batchId` (domain-level idempotency, independent of transport retries). ## Rate limits Same layers as v1: **per IP** (300 req/min) and **per organization** (100 req/min). The real scarce resource for agent runs is the per-org concurrent-agent slot — see `CONCURRENT_LIMIT_EXCEEDED`. | Scope | Limit | |-------|-------| | Per client IP | **300 requests / minute** | | Per organization | **100 requests / minute** | | Concurrent runs (per organization) | **plan-tunable** (1 on starter, 3 on pro, 10 on scale, 20 on ultra, unlimited on enterprise) | Rate-limit headers (`X-RateLimit-Limit-{IP|Global}`, `X-RateLimit-Remaining-*`, `X-RateLimit-Reset-*`) and `Retry-After` are inherited from v1. ## Plan-tier policy Agent-loop knobs (`maxSteps`, `timeoutSeconds`) are **not** request parameters — they're internal config gated by the plan tier. The chat agent's intrinsic 20-minute `AGENT_TIMEOUT_MS` is the wall- clock ceiling. | Plan | Default model | Available models | internal maxSteps | | --- | --- | --- | --- | | `starter` | `origami-lite` | `origami-lite` | 30 | | `pro` | `origami-max` | `origami-lite`, `origami-max` | 50 | | `scale` | `origami-max` | `origami-lite`, `origami-max` | 75 | | `ultra` | `origami-max` | `origami-lite`, `origami-max` | 100 | | `enterprise` | `origami-max` | `origami-lite`, `origami-max` | 200 | ## Errors All error codes are `UPPERCASE_SNAKE_CASE` and live on `code`. The response body always has `error` (human-readable) and `code`; error-specific fields (e.g. `creditsRequired`, `topUpUrl`, `currentRun`, `activeSessions`) live alongside at the top level when they're stable parts of the contract. Non-`completed` terminal *run* states (`timed_out`, `errored`, `cancelled`, `needs_input`, `step_cap_hit`) are surfaced on the `Run` object's single `status` field — not as HTTP error codes. Callers branch on the run object. Notable codes beyond the per-endpoint ones: `IDEMPOTENCY_ERROR` (409 — `Idempotency-Key` reused with a different request, or the original is still in flight), `TOO_MANY_ROWS` (413 — a CSV upsert exceeds the 100-row per-request cap), `CAMPAIGN_NOT_FOUND` (404 — missing or cross-org campaign id), `SEQUENCE_NOT_FOUND` (404 — missing or cross-org sequence id), and `PROJECT_NOT_FOUND` (404 — the `x-origami-project` header names an unknown, cross-parent, or deleted project). ## Webhooks Sequencer events fan out via HTTPS POSTs you configure in **Settings → Developers → Webhooks**. See the [webhooks reference](/webhooks/overview) and the machine-readable [openapi-webhooks.yaml](/openapi-webhooks.yaml) spec for working receiver scaffolding. servers: - url: https://origami.chat/api/v2 description: Production security: - bearerAuth: [] tags: - name: Projects description: Projects (child orgs) under the parent org — list, get, create, update, delete. Managed from the parent; the `x-origami-project` header is ignored on these paths. - name: Agents description: Create, list, and archive AI workers (a.k.a. agents). - name: Runs description: Send prompts to an agent, poll status, and cancel. - name: Tables description: Read table state and lifetime credit cost. - name: Workspace description: Workspaces, tables, columns, rows, cells, enrichment runs, and documents. - name: Campaigns description: First-class outreach campaigns — list, get, read people + stats, create + edit (agentic), launch/pause/resume, delete. - name: Sequences description: Read per-recipient sequences (with steps inline), stop and delete them. Content edits go through the agentic campaign edit. - name: Scheduled agents description: Recurring agents (cron) — CRUD, enable/disable, trigger, run history. - name: Account description: Org account state — plan, capabilities, credit balance. paths: /agents: post: x-mint: href: /agents/reference/create-agent tags: [Agents] summary: Create an agent and admit its first run description: | Creates a new agent (and, if `workspaceId` is null and no `focusTableIds` are passed, an auto-created workspace), inserts a synthetic user message carrying the prompt, claims the per-org concurrent slot, and spawns the agent work in the background. Responds `202 Accepted` with the initial run object (`status: "running"`). Poll `GET /agents/{id}/runs/{runId}` until `status !== "running"`. operationId: createAgentAndRun parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAgentRequest' responses: '202': description: Run admitted; agent work is in the background. content: application/json: schema: type: object required: [agent, run, workspace] properties: agent: { $ref: '#/components/schemas/AgentSummary' } run: { $ref: '#/components/schemas/Run' } workspace: type: object required: [object, id, name, createdAt, createdByApi] properties: object: { type: string, const: workspace } id: { type: string, format: uuid } name: { type: string } createdAt: { type: string, format: date-time } createdByApi: type: boolean description: True when the server auto-created this workspace for the agent. False when the caller supplied `workspaceId`. '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '402': $ref: '#/components/responses/InsufficientCredits' '403': description: Workspace limit reached while auto-creating an API workspace. content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Workspace limit reached (10/10). Pass an existing workspaceId or upgrade your plan. code: WORKSPACE_LIMIT_REACHED '429': $ref: '#/components/responses/ConcurrentLimit' get: tags: [Agents] summary: List agents (cursor-paginated; ?search=) operationId: listAgents x-mint: href: /agents/reference/list-agents parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: search description: Case-insensitive substring match on the agent name. schema: { type: string } responses: '200': description: A page of agents in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Agent' } '401': $ref: '#/components/responses/Unauthorized' /agents/{id}: parameters: - $ref: '#/components/parameters/AgentId' get: tags: [Agents] summary: Get an agent operationId: getAgent x-mint: href: /agents/reference/get-agent responses: '200': description: The agent and its last run summary content: application/json: schema: { $ref: '#/components/schemas/Agent' } '404': $ref: '#/components/responses/AgentNotFound' delete: tags: [Agents] summary: Archive an agent (soft-delete) description: | Soft-deletes the agent. **If the agent's auto-created workspace has no other active agents**, the workspace is also soft-deleted in the same transaction. Hand-supplied workspaces (those passed in `workspaceId` on create) are never auto-deleted. operationId: archiveAgent x-mint: href: /agents/reference/archive-agent responses: '200': description: Archived content: application/json: schema: type: object required: [agent, workspace] properties: agent: type: object required: [object, id, deletedAt] properties: object: { type: string, const: agent } id: { type: string, format: uuid } deletedAt: { type: string, format: date-time } workspace: type: object required: [object, id, deletedAt] properties: object: { type: string, const: workspace } id: { type: string, format: uuid } deletedAt: type: string format: date-time nullable: true description: | ISO timestamp when the workspace cascade-deleted along with the agent, or `null` when it was preserved (hand-supplied, or still has other active agents). Only auto-created workspaces are ever cascade-deleted. '404': $ref: '#/components/responses/AgentNotFound' /agents/{id}/runs: parameters: - $ref: '#/components/parameters/AgentId' post: x-mint: href: /agents/reference/send-run tags: [Runs] summary: Send a follow-up run description: | Admits a new run on an existing agent. Same agent, same workspace, same conversation context. Responds `202 Accepted` with the initial run object; agent work runs in the background. Use this to answer a `needs_input` question — the agent picks up from the prior conversation. operationId: sendRun parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SendRunRequest' } responses: '202': description: Run admitted; agent work is in the background. content: application/json: schema: type: object required: [run] properties: run: { $ref: '#/components/schemas/Run' } '400': $ref: '#/components/responses/ValidationError' '402': $ref: '#/components/responses/InsufficientCredits' '404': $ref: '#/components/responses/AgentNotFound' '409': $ref: '#/components/responses/AgentBusy' '429': $ref: '#/components/responses/ConcurrentLimit' get: tags: [Runs] summary: List runs for an agent (cursor-paginated) operationId: listRuns x-mint: href: /agents/reference/list-runs parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: A page of run summaries in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/RunSummary' } '404': $ref: '#/components/responses/AgentNotFound' /agents/{id}/runs/{runId}: parameters: - $ref: '#/components/parameters/AgentId' - $ref: '#/components/parameters/RunId' get: x-mint: href: /agents/reference/get-run tags: [Runs] summary: Get a run (poll for status / final result) description: | Returns the run object. Works while the run is still running (`status: "running"` with partial `actions[]`) and after it terminates. This is the primary read path — every call after `POST /agents` polls this endpoint until `status !== "running"`. operationId: getRun parameters: - in: query name: include description: | CSV-style list of opt-in projections. Recognized tokens: - `stats` — embed table economics on every `response.tables[]` entry (per-table + per-column). - `transcript` — return the full public transcript on `response.transcript`. Combine: `?include=stats,transcript`. Unknown tokens are ignored. schema: type: string example: stats,transcript responses: '200': description: The run object headers: Retry-After: schema: { type: integer } description: | Poll-pacing hint in seconds, present ONLY while the run is non-terminal (`status: "running"`) — currently `15`. Honor it as the canonical poll cadence. Omitted on terminal responses; that absence is the signal to stop polling. content: application/json: schema: { $ref: '#/components/schemas/Run' } '404': $ref: '#/components/responses/RunNotFound' /agents/{id}/tables: parameters: - $ref: '#/components/parameters/AgentId' get: tags: [Tables] summary: List tables in the agent's workspace description: | The fallback discovery surface in v2. The run object only carries `response.tables[]` for tables a specific run *touched* (empty when the run made no mutations or stopped on a question), so callers that need "every table this agent has access to" use this endpoint instead. Each entry is a full TableObject — same shape as `GET /api/v2/tables/{id}` and the same shape embedded under `Run.response.tables[]`. Pass `?include=stats` to attach the economics block (per-table + per-column). Scoped to the agent's own workspace; a caller cannot probe arbitrary workspaces through this path. Returned in the canonical list envelope with `nextCursor` always `null` — an agent's workspace is rarely more than a handful of tables, so the endpoint does not paginate. operationId: listAgentTables x-mint: href: /agents/reference/list-agent-tables parameters: - in: query name: include description: | Pass `stats` to attach per-table economics on every entry (creditsPerLead, qualification, funnel, lead sources). schema: type: string example: stats responses: '200': description: Tables in the agent's workspace (list envelope + `workspaceId`). content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items, workspaceId] properties: items: type: array items: { $ref: '#/components/schemas/Table' } workspaceId: { type: string, format: uuid } '404': $ref: '#/components/responses/AgentNotFound' /tables/{tableId}: parameters: - $ref: '#/components/parameters/TableId' get: x-mint: href: /agents/reference/get-table tags: [Tables] summary: Get a table — name, leadCount, columns, credits, optional economics description: | The canonical "what's in this table and what did it cost?" surface in v2. Same shape as the entries in `Run.response.tables[]` and `GET /agents/{id}/tables`. Default shape is lite — `leadCount`, `columns[]` with per-column `credits.lifetimeUsed`, table-level `credits.lifetimeUsed` (sum of the column breakdown). Pass `?include=stats` to attach the economics block — the same numbers the UI shows at the top of the table (creditsPerLead, qualification breakdown, funnel, lead sources, ...). `credits.lifetimeUsed` accumulates across every agent run that ever populated cells on this table — it is the unit a human thinks in when they ask how much a list cost. It does NOT include still-active cell-run reservations; wait for the relevant run to terminate before reading the final number. Scoped to the API key's org. There is no API-owned filter on tables, so any non-deleted table in your org is fetchable. For programmatic row reads, use [`GET /api/v2/tables/{tableId}/rows`](https://origami.chat/reading-data). operationId: getTable parameters: - in: query name: include description: | Pass `stats` to attach the economics block (creditsPerLead, qualification, funnel, lead sources) plus per-column `stats` and `qualification`. schema: type: string example: stats responses: '200': description: The table object content: application/json: schema: { $ref: '#/components/schemas/Table' } '404': description: Table not found in this org (or soft-deleted) content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Table not found code: TABLE_NOT_FOUND /agents/{id}/cancel: parameters: - $ref: '#/components/parameters/AgentId' post: x-mint: href: /agents/reference/cancel-run tags: [Runs] summary: Cancel the agent's currently-active run (idempotent, cooperative) description: | The per-agent mutex guarantees at most one in-flight run per agent, so cancel takes only the agentId. Wraps the same machinery the UI's stop route uses (`markCancelling` + Redis `cancelStream` publish). Idempotent: returns `200` whether the agent has an active run, is already cancelling, or has no in-flight run at all. The response body always carries the latest run object — the just-cancelled one if there was one, otherwise the agent's most recent terminal run. operationId: cancelActiveRun parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: | The (possibly updated) run object, or `{ "run": null }` when the agent has never had a run. content: application/json: schema: type: object required: [run] properties: run: nullable: true allOf: - $ref: '#/components/schemas/Run' '404': $ref: '#/components/responses/AgentNotFound' # ── Workspace segment ──────────────────────────────────────────── /workspaces: get: tags: [Workspace] summary: List workspaces (cursor-paginated; ?search=) operationId: listWorkspaces x-mint: href: /agents/reference/list-workspaces parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: search description: Case-insensitive substring match on the workspace name. schema: { type: string } responses: '200': description: A page of workspaces in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Workspace' } post: tags: [Workspace] summary: Bootstrap a workspace (upload-first flows) operationId: createWorkspace x-mint: href: /agents/reference/create-workspace parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: application/json: schema: type: object properties: name: { type: string, maxLength: 80 } responses: '201': description: The created workspace. content: application/json: schema: { $ref: '#/components/schemas/Workspace' } '403': { description: WORKSPACE_LIMIT_REACHED (carries an upgrade-plan handoff). } /workspaces/{workspaceId}: parameters: - $ref: '#/components/parameters/WorkspaceId' get: tags: [Workspace] summary: Fetch a workspace operationId: getWorkspace x-mint: href: /agents/reference/get-workspace responses: '200': description: The workspace object. content: application/json: schema: { $ref: '#/components/schemas/Workspace' } '404': { description: WORKSPACE_NOT_FOUND. } delete: tags: [Workspace] summary: Delete a workspace (two-step; ?confirm=true) description: | Permanently deletes a workspace and its entire entity tree (tables, rows, cells, sequences, chat history, documents). Follows the v2 deletes convention — deliberately **two-step** so it can't fire on a single ambiguous instruction: 1. Call **without** `?confirm=true` → **no deletion**. Returns HTTP 200 with the impact preview `{ workspaceId, name, confirmationRequired: true, willDelete: { tables, rows } }`. The caller is expected to check with the user before proceeding. 2. After the user agrees, retry with `?confirm=true` → performs the cascade soft-delete and returns `{ workspaceId, name, deleted: true }`. A missing / cross-org / already-deleted workspace returns `404 WORKSPACE_NOT_FOUND`. operationId: deleteWorkspaceRest x-mint: href: /agents/reference/delete-workspace parameters: - in: query name: confirm description: Must be `true` to actually delete. Omit for the impact preview. schema: { type: boolean, default: false } responses: '200': description: The impact preview (unconfirmed) or the delete receipt (confirmed). content: application/json: schema: oneOf: - $ref: '#/components/schemas/WorkspaceDeletePreview' - type: object required: [workspaceId, name, deleted] properties: workspaceId: { type: string, format: uuid } name: { type: string } deleted: { type: boolean, const: true } '404': { description: WORKSPACE_NOT_FOUND. } /workspaces/{workspaceId}/delete: parameters: - $ref: '#/components/parameters/WorkspaceId' post: deprecated: true tags: [Workspace] summary: 'Legacy alias: delete a workspace (body-confirm protocol)' description: | Deprecated alias for `DELETE /workspaces/{workspaceId}`. Keeps the original body-confirm protocol: with `confirm` omitted/false it returns `409 CONFIRMATION_REQUIRED` whose `details` carry the workspace name and the `willDelete: { tables, rows }` impact preview; the `{ "confirm": true }` retry performs the delete and returns `{ workspaceId, name, deleted: true }`. operationId: deleteWorkspace x-mint: href: /agents/reference/delete-workspace-legacy parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: false content: application/json: schema: type: object properties: confirm: type: boolean default: false description: Must be true to actually delete. Omit/false to get the confirmation preview. responses: '200': { description: '{ workspaceId, name, deleted: true } — workspace deleted.' } '409': { description: 'CONFIRMATION_REQUIRED — details carry name + willDelete preview; retry with { confirm: true }.' } '404': { description: WORKSPACE_NOT_FOUND. } /workspaces/{workspaceId}/documents: parameters: - $ref: '#/components/parameters/WorkspaceId' get: tags: [Workspace] summary: List workspace-scoped documents (cursor-paginated) operationId: listWorkspaceDocuments x-mint: href: /agents/reference/list-documents parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: A page of document summaries in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/DocumentSummary' } '404': { description: WORKSPACE_NOT_FOUND. } post: tags: [Workspace] summary: Upload files into a workspace (the single ingest verb) description: | The one ingest path for every file type — base64 file bytes inside JSON (no multipart). CSVs become new tables (`mode: "table"`, the default for `.csv`), append into an existing table (`mode: "append"` + `tableId`), or store as documents (`mode: "document"`); any other extension is stored as a document. Preflight validation is all-or-nothing: the whole request is rejected (count cap, per-file mode/extension/append-target) before any file is ingested. Per-file ingestion can still fail after preflight — the response is `201` when at least one file landed and `422 UPLOAD_FAILED` when every file errored. Replaces the deprecated `POST /workspaces/{workspaceId}/uploads` spelling (same contract). operationId: uploadWorkspaceDocuments x-mint: href: /agents/reference/upload-documents parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UploadFilesRequest' } responses: '201': { description: 'Per-file ingest results ({ results: [...] }); individual entries may be kind: "error".' } '400': { description: 'VALIDATION_ERROR / INVALID_UPLOAD_MODE.' } '404': { description: 'WORKSPACE_NOT_FOUND / TABLE_NOT_FOUND (append target).' } '413': { description: TOO_MANY_FILES — over the per-request file cap. } '422': { description: UPLOAD_FAILED — every file errored; details carry the per-file results. } /workspaces/{workspaceId}/uploads: parameters: - $ref: '#/components/parameters/WorkspaceId' post: deprecated: true tags: [Workspace] summary: 'Legacy alias: upload files (use POST /workspaces/{workspaceId}/documents)' description: | Deprecated alias for `POST /workspaces/{workspaceId}/documents` — identical body and responses. operationId: uploadWorkspaceFilesLegacy x-mint: href: /agents/reference/upload-documents-legacy parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UploadFilesRequest' } responses: '201': { description: 'Per-file ingest results ({ results: [...] }).' } '413': { description: TOO_MANY_FILES. } '422': { description: UPLOAD_FAILED. } /workspaces/{workspaceId}/documents/{documentId}: parameters: - $ref: '#/components/parameters/WorkspaceId' - $ref: '#/components/parameters/DocumentId' get: tags: [Workspace] summary: Read a document (metadata + content) operationId: getWorkspaceDocument x-mint: href: /agents/reference/get-document responses: '200': description: The document detail with inline content. content: application/json: schema: { $ref: '#/components/schemas/DocumentDetail' } '404': { description: DOCUMENT_NOT_FOUND. } patch: tags: [Workspace] summary: Rename a document description: | DB-only rename — re-slugs the basename from `name` while preserving the directory prefix and extension; the document id is stable. A collision with another live document at the derived path → `409 DOCUMENT_PATH_TAKEN`. operationId: renameWorkspaceDocument x-mint: href: /agents/reference/rename-document requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, minLength: 1, maxLength: 200 } responses: '200': description: The renamed document summary. content: application/json: schema: { $ref: '#/components/schemas/DocumentSummary' } '404': { description: DOCUMENT_NOT_FOUND. } '409': { description: DOCUMENT_PATH_TAKEN. } delete: tags: [Workspace] summary: Delete a workspace document operationId: deleteWorkspaceDocument x-mint: href: /agents/reference/delete-document responses: '200': { description: '{ documentId, deleted: true }.' } '404': { description: DOCUMENT_NOT_FOUND. } /workspaces/{workspaceId}/documents/{documentId}/rename: parameters: - $ref: '#/components/parameters/WorkspaceId' - $ref: '#/components/parameters/DocumentId' post: deprecated: true tags: [Workspace] summary: 'Legacy alias: rename a document (use PATCH .../documents/{documentId})' description: | Deprecated alias for `PATCH /workspaces/{workspaceId}/documents/{documentId}` — identical body and responses. operationId: renameWorkspaceDocumentLegacy x-mint: href: /agents/reference/rename-document-legacy parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, minLength: 1, maxLength: 200 } responses: '200': description: The renamed document summary. content: application/json: schema: { $ref: '#/components/schemas/DocumentSummary' } '404': { description: DOCUMENT_NOT_FOUND. } '409': { description: DOCUMENT_PATH_TAKEN. } /tables: get: tags: [Workspace] summary: List tables (cursor-paginated; ?workspaceId=) operationId: listTables x-mint: href: /agents/reference/list-tables parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: workspaceId description: Scope the list to one workspace. schema: { type: string, format: uuid } responses: '200': description: A page of TableObjects in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Table' } /tables/{tableId}/columns: parameters: - $ref: '#/components/parameters/TableId' get: tags: [Workspace] summary: List a table's columns operationId: listColumns x-mint: href: /agents/reference/list-columns responses: '200': description: | Column objects in the canonical list envelope. Not paginated — `nextCursor` is always `null`. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/WorkspaceColumn' } '404': { description: TABLE_NOT_FOUND. } /tables/{tableId}/rows: parameters: - $ref: '#/components/parameters/TableId' get: tags: [Workspace] summary: List rows (typed cells; ?cells=flat / ?format=csv) description: | Returns Row objects (`{ object: "row", id, cells }`) with polymorphic typed cells by default (`scalar` / `value` (+run) / `sequence`), in the canonical list envelope **plus `total`** (the filtered row count for the query's scope). `?cells=flat` returns the v1-style `{ slug: value }` rows (deliberately unstamped); `?format=csv` streams CSV. Supports slug-keyed `filters` / `sort` JSON and cursor pagination — this endpoint allows `limit` up to **200**. operationId: listRows x-mint: href: /agents/reference/list-rows parameters: - $ref: '#/components/parameters/Cursor' - in: query name: limit description: Max rows to return. Default 50; rows allow up to 200 (vs. 100 elsewhere). schema: { type: integer, minimum: 1, maximum: 200, default: 50 } - in: query name: filters description: | JSON array of `{ column, operator, value }` filters. `column` is an input-column slug; operators: `contains`, `not_contains`, `equals`, `not_equals`, `is_empty`, `is_not_empty`, `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`. Unknown columns → `UNKNOWN_COLUMN`. schema: { type: string } - in: query name: sort description: 'JSON object `{ column, direction: "asc"|"desc" }` (slug-keyed).' schema: { type: string } - in: query name: cells description: 'Pass `flat` for v1-style `{ slug: value }` rows instead of typed cells.' schema: { type: string, enum: [flat] } - in: query name: format description: Pass `csv` to stream the (flat) rows as CSV. schema: { type: string, enum: [csv] } - in: query name: defaults description: Pass `false` to disable the table's default filters/sort. schema: { type: string, enum: ['true', 'false'] } responses: '200': description: A page of rows (list envelope + `total`). content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items, total] properties: items: type: array items: { $ref: '#/components/schemas/Row' } total: type: integer description: Total row count for the query's scope (all pages). text/csv: schema: { type: string } '400': { description: 'INVALID_FILTERS / INVALID_SORT / UNKNOWN_COLUMN / VALIDATION_ERROR.' } '404': { description: TABLE_NOT_FOUND. } delete: tags: [Workspace] summary: Bulk soft-delete rows by id description: | Soft-deletes the given rows (up to **100** per call). Each row and its cells / cell_runs / active sequences cascade off. Ids that don't belong to this table/org, or are already deleted, are skipped — so `deleted` may be less than `requested` (an idempotent re-delete returns `deleted: 0`). Deleted rows are recoverable via the app's restore flow. operationId: deleteRows x-mint: href: /agents/reference/delete-rows requestBody: required: true content: application/json: schema: type: object required: [rowIds] properties: rowIds: type: array items: { type: string, format: uuid } minItems: 1 maxItems: 100 responses: '200': description: Delete receipt. content: application/json: schema: type: object required: [deleted, requested] properties: deleted: type: integer description: Rows actually soft-deleted (live rows in this table + org that matched). requested: type: integer description: Row ids submitted (raw count, before de-duplication). example: deleted: 2 requested: 2 '400': { description: VALIDATION_ERROR (rowIds empty, non-UUID, or over 100). } '404': { description: TABLE_NOT_FOUND. } /tables/{tableId}/rows/upsert: parameters: - $ref: '#/components/parameters/TableId' post: tags: [Workspace] summary: Upsert rows on matchColumns (the single v2 batch write primitive) description: | v2's one row-write endpoint — there is no bare per-row insert (an insert-only call is just an upsert whose rows match nothing). All writes go through `api_batches` and are idempotent per `batchId`. Rows matching an existing row on **every** `matchColumns` value update that row's input cells; non-matching rows insert. Per-row `inserted`/`updated`/`skipped` outcomes are recorded on the batch. - Row keys and `matchColumns` are **input column slugs** (from `GET /tables/{tableId}/columns` → `columns[].slug`), never display names. A non-slug row key → `UNKNOWN_FIELDS`. - `matchColumns` (required) must be real input columns (`UNKNOWN_COLUMN`) and every match value must be present + non-empty on every row (`MISSING_MATCH_VALUE`). - Duplicate request keys → `DUPLICATE_MATCH_KEY`; multiple existing rows match one key → `AMBIGUOUS_MATCH` (409). - `enrich` (default true) enriches freshly **inserted** rows. - `reenrichUpdated` (default false) — set true to also re-run enrichment on rows the upsert **updated** (re-spends credits); off by default so an upsert never silently re-enriches matched rows. Only `input`-kind columns are writable. Enrichment, score, and **sequence** columns are populated automatically — sequences are agent/column-generated only (see `POST /tables/{tableId}/campaigns`), never set via this endpoint; passing one returns `NON_INPUT_COLUMNS`. The response references the tracking object by id: it is an `enrichment_run` the caller polls via `GET /enrichment-runs/{runId}`. operationId: upsertRows x-mint: href: /agents/reference/upsert-rows parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [rows, matchColumns] properties: rows: { type: array, items: { type: object, additionalProperties: true }, minItems: 1, maxItems: 100 } matchColumns: { type: array, items: { type: string }, minItems: 1 } enrich: { type: boolean, default: true } reenrichUpdated: { type: boolean, default: false } batchId: { type: string, format: uuid } responses: '201': description: The upsert receipt — an `enrichment_run` reference with per-outcome counts. content: application/json: schema: { $ref: '#/components/schemas/UpsertReceipt' } '200': description: Idempotent replay of an existing `batchId` (same receipt shape). content: application/json: schema: { $ref: '#/components/schemas/UpsertReceipt' } /tables/{tableId}/rows/upsert-file: parameters: - $ref: '#/components/parameters/TableId' post: tags: [Workspace] summary: Upsert rows from a CSV file (same operation, CSV transport) description: | The CSV transport for `POST /tables/{tableId}/rows/upsert`: `content` is a base64-encoded CSV whose headers are input-column slugs (the same keys as JSON upsert rows) with one record per data row. Identical match/enrich semantics, identical per-`batchId` idempotency, identical row cap — a CSV with more than **100** data rows is rejected with `413 TOO_MANY_ROWS` (split the file or page the JSON upsert). operationId: upsertRowsFromFile x-mint: href: /agents/reference/upsert-rows-file parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [content, matchColumns] properties: content: type: string description: Base64-encoded CSV. Headers are input-column slugs. filename: { type: string, minLength: 1, maxLength: 255 } matchColumns: { type: array, items: { type: string }, minItems: 1 } enrich: { type: boolean, default: true } reenrichUpdated: { type: boolean, default: false } batchId: { type: string, format: uuid } responses: '201': description: The upsert receipt — an `enrichment_run` reference with per-outcome counts. content: application/json: schema: { $ref: '#/components/schemas/UpsertReceipt' } '200': description: Idempotent replay of an existing `batchId` (same receipt shape). content: application/json: schema: { $ref: '#/components/schemas/UpsertReceipt' } '400': { description: 'VALIDATION_ERROR — bad base64 / undecodable or empty CSV.' } '413': description: TOO_MANY_ROWS — the CSV exceeds 100 data rows; details carry `{ rows, limit }`. '404': { description: TABLE_NOT_FOUND. } /tables/{tableId}/cancel: parameters: - $ref: '#/components/parameters/TableId' post: tags: [Workspace] summary: Stop active cell work for a table (the API "stop" button) description: | Cancel every active (waiting / queued / running) `cell_run` for the table — the API equivalent of the in-app "stop" button. Running cells finish their in-flight execution but skip further work once they observe the cancelled status; sent history is never touched. - Omit `columns` to stop the whole table; a full-table stop **also** cancels active lead-source jobs. - Pass `columns` (an array of **column slugs** from `GET /tables/{tableId}/columns` → `columns[].slug`) to scope the stop to those columns only. A column scope deliberately leaves lead-source jobs running (they are table-wide, not per-column). An unknown slug → `UNKNOWN_COLUMN`; an empty array → `VALIDATION_ERROR`. - `dryRun: true` reports how much is active right now (`{ dryRun, tableId, activeCells, activeLeadSourceJobs }`) with zero writes — the "what's running?" probe. `activeCells` respects the `columns` scope; `activeLeadSourceJobs` is always the table-wide count (lead-source jobs aren't per-column), so a scoped dry-run still tells you the truth about table activity. This stops enrichment/score/sequence-drafting cell runs. To halt an already-launched outreach use `POST /sequences/{sequenceId}/stop` (one recipient) or `POST /campaigns/{campaignId}/pause` (the whole campaign). operationId: cancelTableCells x-mint: href: /agents/reference/cancel-table-cells parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: false content: application/json: schema: type: object properties: columns: type: array items: { type: string, minLength: 1, pattern: '\S' } minItems: 1 description: Column slugs to scope the stop to. Omit to stop every column. dryRun: { type: boolean, default: false } responses: '200': { description: '{ tableId, cancelledCells, cancelledLeadSourceJobs } — or the dryRun report.' } '400': { description: 'VALIDATION_ERROR / UNKNOWN_COLUMN.' } '404': { description: TABLE_NOT_FOUND. } '500': { description: 'CANCEL_PARTIALLY_FAILED — one leg failed; details carry the partial cancelledCells/cancelledLeadSourceJobs + failures[].' } /tables/{tableId}/rows/{rowId}: parameters: - $ref: '#/components/parameters/TableId' - $ref: '#/components/parameters/RowId' get: tags: [Workspace] summary: Fetch one row operationId: getRow x-mint: href: /agents/reference/get-row responses: '200': description: The Row object with typed cells keyed by column slug. content: application/json: schema: { $ref: '#/components/schemas/Row' } '404': { description: ROW_NOT_FOUND / TABLE_NOT_FOUND. } /tables/{tableId}/rows/{rowId}/cells/{columnId}: parameters: - $ref: '#/components/parameters/TableId' - $ref: '#/components/parameters/RowId' - $ref: '#/components/parameters/ColumnId' get: tags: [Workspace] summary: Fetch one cell operationId: getCell x-mint: href: /agents/reference/get-cell responses: '200': { description: The single typed cell (+ run metadata where present). } '404': { description: COLUMN_NOT_FOUND / ROW_NOT_FOUND / TABLE_NOT_FOUND. } /enrichment-runs: get: tags: [Workspace] summary: List enrichment runs (cursor-paginated; ?tableId=) description: | Org-wide list of enrichment runs — the tracked batches of column-over-row work (upserts, file ingests) — newest first. Optional `?tableId=` filter. `GET /batches` is the deprecated alias. operationId: listEnrichmentRuns x-mint: href: /agents/reference/list-enrichment-runs parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: tableId description: Scope the list to one table. schema: { type: string, format: uuid } responses: '200': description: A page of enrichment-run summaries in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/EnrichmentRun' } '400': { description: VALIDATION_ERROR — malformed tableId. } /enrichment-runs/{runId}: parameters: - $ref: '#/components/parameters/EnrichmentRunId' get: tags: [Workspace] summary: Track an enrichment run (incl. per-row upsert outcomes) description: | Run status, row count, enrichment counts, credits used, and — for `upsert`-type runs — the per-row `outcomes[]` ledger plus `outcomeCounts`. `GET /batches/{batchId}` is the deprecated alias. operationId: getEnrichmentRun x-mint: href: /agents/reference/get-enrichment-run responses: '200': description: The enrichment-run detail. content: application/json: schema: { $ref: '#/components/schemas/EnrichmentRunDetail' } '404': { description: BATCH_NOT_FOUND — enrichment run not found. } /tables/{tableId}/enrichment-runs: parameters: - $ref: '#/components/parameters/TableId' get: tags: [Workspace] summary: List a table's enrichment runs (cursor-paginated) description: | Same list as `GET /enrichment-runs?tableId=`, but the table is resolved org-scoped first — a missing or cross-org table returns `404 TABLE_NOT_FOUND` instead of an empty list. operationId: listTableEnrichmentRuns x-mint: href: /agents/reference/list-table-enrichment-runs parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: A page of enrichment-run summaries in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/EnrichmentRun' } '404': { description: TABLE_NOT_FOUND. } /batches: get: deprecated: true tags: [Workspace] summary: 'Legacy alias: list enrichment runs (use GET /enrichment-runs)' description: Deprecated alias for `GET /enrichment-runs` — identical parameters and response. operationId: listBatches x-mint: href: /agents/reference/list-batches parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: tableId schema: { type: string, format: uuid } responses: '200': description: A page of enrichment-run summaries in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/EnrichmentRun' } /batches/{batchId}: parameters: - $ref: '#/components/parameters/BatchId' get: deprecated: true tags: [Workspace] summary: 'Legacy alias: track an enrichment run (use GET /enrichment-runs/{runId})' description: Deprecated alias for `GET /enrichment-runs/{runId}` — identical response. operationId: getBatch x-mint: href: /agents/reference/get-batch responses: '200': description: The enrichment-run detail. content: application/json: schema: { $ref: '#/components/schemas/EnrichmentRunDetail' } '404': { description: BATCH_NOT_FOUND. } # ── Outreach segment — campaigns, sequences, steps ────────────── /sequences: get: tags: [Sequences] summary: List sequences (bounded scope, cursor-paginated, filtered) description: | Requires one of `workspaceId` / `tableId` / `columnId` (else `400 MISSING_SCOPE`). Optional `status` / `channel` / `recipient` filters compose with `cursor` / `limit`. operationId: listSequences x-mint: href: /agents/reference/list-sequences parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: workspaceId schema: { type: string, format: uuid } - in: query name: tableId schema: { type: string, format: uuid } - in: query name: columnId schema: { type: string, format: uuid } - in: query name: status schema: { type: string } - in: query name: channel schema: { type: string } - in: query name: recipient schema: { type: string } responses: '200': description: A page of sequence summaries in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SequenceSummary' } '400': { description: MISSING_SCOPE / VALIDATION_ERROR. } /tables/{tableId}/sequences: parameters: - $ref: '#/components/parameters/TableId' get: tags: [Sequences] summary: List a table's sequences (cursor-paginated, filtered) description: | Sequences scoped to one table — the quick "does this table have any sequences?" read. Same filters (status / channel / recipient) and pagination as `GET /sequences`, but the table is resolved org-scoped first, so a missing or cross-org table returns 404 TABLE_NOT_FOUND instead of an empty list. operationId: listTableSequences x-mint: href: /agents/reference/list-table-sequences parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: status schema: { type: string } - in: query name: channel schema: { type: string } - in: query name: recipient schema: { type: string } responses: '200': description: A page of sequence summaries for the table in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/SequenceSummary' } '404': { description: TABLE_NOT_FOUND. } post: deprecated: true tags: [Sequences] summary: 'Legacy alias: create a campaign (use POST /tables/{tableId}/campaigns)' description: | Deprecated alias for `POST /tables/{tableId}/campaigns` — identical body and responses (agentic campaign creation from `{ instructions }`, `202` with `{ agent, run, table }`). operationId: createTableSequence x-mint: href: /agents/reference/create-sequence-legacy parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [instructions] properties: instructions: type: string minLength: 1 maxLength: 10000 description: Free-form description of the campaign to draft. responses: '202': { description: '{ agent, run, table } — run admitted, work runs in background.' } '400': { description: VALIDATION_ERROR. } '402': { description: INSUFFICIENT_CREDITS. } '404': { description: TABLE_NOT_FOUND. } '409': { description: AGENT_BUSY. } '429': { description: CONCURRENT_LIMIT_EXCEEDED. } /sequences/{sequenceId}: parameters: - $ref: '#/components/parameters/SequenceId' get: tags: [Sequences] summary: Fetch a sequence with steps inline (provider internals redacted) operationId: getSequence x-mint: href: /agents/reference/get-sequence responses: '200': description: The sequence detail with its steps. content: application/json: schema: { $ref: '#/components/schemas/SequenceDetail' } '404': { description: SEQUENCE_NOT_FOUND. } delete: tags: [Sequences] summary: Delete a sequence (?force=true, ?dryRun=true) description: | Soft-deletes one sequence. Without `?force=true`, a sequence with a sent footprint is guarded — `409 SEQUENCE_HAS_SENT_MESSAGES`; `?force=true` (`forceDeletionOfSentMessages`) extends the delete to already-sent messages. `?dryRun=true` reports the would-be effect (`{ dryRun, sequenceId, status, force }`) with no writes. Replaces the deprecated `POST /sequences/{sequenceId}/delete` spelling. operationId: deleteSequenceRest x-mint: href: /agents/reference/delete-sequence parameters: - in: query name: force description: Also delete sequences with already-sent messages. schema: { type: boolean, default: false } - in: query name: dryRun description: Report the would-be effect; write nothing. schema: { type: boolean, default: false } responses: '200': { description: '{ sequenceId, deleted: true } — or the dryRun report.' } '409': { description: SEQUENCE_HAS_SENT_MESSAGES. } '404': { description: SEQUENCE_NOT_FOUND. } /sequences/{sequenceId}/stop: parameters: - $ref: '#/components/parameters/SequenceId' post: tags: [Sequences] summary: Stop a sequence (header stop; sent history preserved) description: | Branches: nothing-to-stop (fresh draft) / stopped / noop (already stopped). `?dryRun=true` reports the would-be effect with no writes. operationId: stopSequence x-mint: href: /agents/reference/stop-sequence parameters: - $ref: '#/components/parameters/IdempotencyKey' - in: query name: dryRun schema: { type: boolean, default: false } responses: '200': { description: '{ sequenceId, action } — or the dryRun report.' } '404': { description: SEQUENCE_NOT_FOUND. } /sequences/{sequenceId}/delete: parameters: - $ref: '#/components/parameters/SequenceId' post: deprecated: true tags: [Sequences] summary: 'Legacy alias: delete a sequence (use DELETE /sequences/{sequenceId})' description: | Deprecated alias for `DELETE /sequences/{sequenceId}`. Body-flag protocol: soft-delete; `409 SEQUENCE_HAS_SENT_MESSAGES` unless the body sets `forceDeletionOfSentMessages: true`. Supports body `dryRun: true`. operationId: deleteSequence x-mint: href: /agents/reference/delete-sequence-legacy parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '200': { description: '{ sequenceId, deleted: true }.' } '409': { description: SEQUENCE_HAS_SENT_MESSAGES. } '404': { description: SEQUENCE_NOT_FOUND. } # ── Campaigns (first-class) ────────────────────────────────────── /workspaces/{workspaceId}/campaigns: parameters: - $ref: '#/components/parameters/WorkspaceId' get: tags: [Campaigns] summary: List a workspace's campaigns description: | A campaign is a first-class `campaigns` row (org-global, workspace-homed) whose queue is the set of sequences stamped with its id — one sequence per person. Campaigns are homed on a workspace, so the workspace is the primary list scope. Returns the canonical list envelope with `nextCursor: null` (one page, newest first). operationId: listWorkspaceCampaigns x-mint: href: /agents/reference/list-workspace-campaigns responses: '200': description: The workspace's campaigns as summaries in the list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/CampaignSummary' } '404': { description: WORKSPACE_NOT_FOUND. } /tables/{tableId}/campaigns: parameters: - $ref: '#/components/parameters/TableId' get: tags: [Campaigns] summary: List campaigns that send from a table description: | The campaigns whose enrolled sequences send from this table. A campaign owns no table — its `tableId` is derived from the dominant table of its enrolled sequences — so this resolves the table's active campaign(s). Returns the canonical list envelope with `nextCursor: null`. operationId: listTableCampaigns x-mint: href: /agents/reference/list-table-campaigns responses: '200': description: The table's campaigns as summaries in the list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/CampaignSummary' } '404': { description: TABLE_NOT_FOUND. } post: tags: [Campaigns] summary: Create a campaign on a table (agentic) description: | Agentic campaign creation. Minimal body `{ instructions }` — no column or channel; campaigns can be multi-channel and the agent infers the rest from the instructions and the table. Delegates to the chat agent via the v2 run path and responds `202 Accepted` with `{ agent, run, table }` (poll `GET /agents/{agentId}/runs/{runId}` for progress/result). The agent creates the campaign and drafts its sequences (it does not send); once the run completes, the drafted campaign shows up under `GET /tables/{tableId}/campaigns`. The pre-restructure `POST /tables/{tableId}/sequences` spelling is the legacy alias. operationId: createTableCampaign x-mint: href: /agents/reference/create-campaign parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [instructions] properties: instructions: type: string minLength: 1 maxLength: 10000 description: Free-form description of the campaign to draft. responses: '202': description: Run admitted; the agent drafts the campaign in the background. content: application/json: schema: type: object required: [agent, run, table] properties: agent: { $ref: '#/components/schemas/AgentSummary' } run: { $ref: '#/components/schemas/Run' } table: type: object required: [object, id, name] properties: object: { type: string, const: table } id: { type: string, format: uuid } name: { type: string } '400': { description: VALIDATION_ERROR. } '402': { description: INSUFFICIENT_CREDITS. } '404': { description: TABLE_NOT_FOUND. } '409': { description: AGENT_BUSY. } '429': { description: CONCURRENT_LIMIT_EXCEEDED. } /campaigns/{campaignId}: parameters: - $ref: '#/components/parameters/CampaignId' get: tags: [Campaigns] summary: Fetch a campaign operationId: getCampaignById x-mint: href: /agents/reference/get-campaign responses: '200': description: The Campaign object. content: application/json: schema: { $ref: '#/components/schemas/Campaign' } '404': { description: CAMPAIGN_NOT_FOUND. } delete: tags: [Campaigns] summary: Delete a campaign (?confirm=true, ?dryRun=true) description: | Soft-deletes the campaign (instantly halting its picker) and cancels its orphaned sequences. Follows the v2 deletes convention: - Without `?confirm=true` (or with `?dryRun=true`) → HTTP 200 with the impact preview `{ id, name, confirmationRequired: true, status }`; nothing removed. - With `?confirm=true` → deletes and returns `{ id, name, deleted: true }`. operationId: deleteCampaign x-mint: href: /agents/reference/delete-campaign parameters: - in: query name: confirm description: Must be `true` to actually delete. Omit for the impact preview. schema: { type: boolean, default: false } - in: query name: dryRun description: Force the impact preview even when confirm is set. schema: { type: boolean, default: false } responses: '200': description: The impact preview (unconfirmed / dryRun) or the delete receipt (confirmed). content: application/json: schema: oneOf: - $ref: '#/components/schemas/CampaignDeletePreview' - type: object required: [id, name, deleted] properties: id: { type: string, format: uuid } name: { type: string } deleted: { type: boolean, const: true } '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/people: parameters: - $ref: '#/components/parameters/CampaignId' get: tags: [Campaigns] summary: List the people in a campaign (keyset-paginated, filtered) description: | The people enrolled in the campaign — one row per enrolled sequence (recipient), with send status, fit score / explanation, and identity profile. Keyset-paginated via an opaque `cursor`; the list envelope additionally carries a top-level `total`. `GET /campaigns/{campaignId}/sequences` is a synonym (same shape) — in the campaign model a person IS a sequence. operationId: listCampaignPeople x-mint: href: /agents/reference/list-campaign-people parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: search description: Substring match over recipient / identity. schema: { type: string } - in: query name: status description: CSV of send-status buckets to include. schema: { type: string } responses: '200': description: A page of campaign people in the list envelope (with top-level `total`). content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items, total] properties: items: type: array items: { $ref: '#/components/schemas/CampaignPerson' } total: type: integer description: Total enrolled people matching the filters (across all pages). '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/sequences: parameters: - $ref: '#/components/parameters/CampaignId' get: tags: [Campaigns] summary: 'Synonym of GET /campaigns/{campaignId}/people' description: | Identical to `GET /campaigns/{campaignId}/people` — same `campaign_person` item shape, same `search` / `status` (CSV) / `cursor` / `limit` params, same top-level `total`. In the campaign model a person IS a sequence. operationId: listCampaignSequences x-mint: href: /agents/reference/list-campaign-sequences parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: search schema: { type: string } - in: query name: status schema: { type: string } responses: '200': description: A page of campaign people in the list envelope (with top-level `total`). content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items, total] properties: items: type: array items: { $ref: '#/components/schemas/CampaignPerson' } total: { type: integer } '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/stats: parameters: - $ref: '#/components/parameters/CampaignId' get: tags: [Campaigns] summary: Campaign performance stats operationId: getCampaignStats x-mint: href: /agents/reference/get-campaign-stats responses: '200': description: The campaign_stats object. content: application/json: schema: { $ref: '#/components/schemas/CampaignStats' } '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/edits: parameters: - $ref: '#/components/parameters/CampaignId' post: tags: [Campaigns] summary: Request a content change to a campaign (agentic) description: | The ONLY way to change campaign content (message templates, per-lead copy, the brief). Minimal body `{ instructions }` — a natural- language change request the chat agent fulfils. Delegates to the v2 run path and responds `202 Accepted` with `{ agent, run, campaign }` (poll `GET /agents/{agentId}/runs/{runId}`). The API never mutates a template or step directly. operationId: editCampaign x-mint: href: /agents/reference/edit-campaign parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [instructions] properties: instructions: type: string minLength: 1 maxLength: 10000 description: Free-form description of the content change to apply. responses: '202': description: Run admitted; the agent applies the change in the background. content: application/json: schema: type: object required: [agent, run, campaign] properties: agent: { $ref: '#/components/schemas/AgentSummary' } run: { $ref: '#/components/schemas/Run' } campaign: type: object required: [object, id, name] properties: object: { type: string, const: campaign } id: { type: string, format: uuid } name: { type: string } '400': { description: VALIDATION_ERROR. } '402': { description: INSUFFICIENT_CREDITS. } '404': { description: CAMPAIGN_NOT_FOUND. } '409': { description: AGENT_BUSY. } '429': { description: CONCURRENT_LIMIT_EXCEEDED. } /campaigns/{campaignId}/launch: parameters: - $ref: '#/components/parameters/CampaignId' post: tags: [Campaigns] summary: Launch a campaign — activate + run the launch pipeline (alias /send) description: | Marks the campaign ready: sets `status: active` and runs the full launch pipeline campaign-keyed (sender gate, duplicate auto-cancel, per-account scheduling). Idempotent on an already-active campaign. **Launch is "mark ready", never "force".** There are NO override knobs — send windows, daily caps, spacing, and duplicate settings are the campaign's own persisted config, set through the agent, never per-call. `POST /campaigns/{campaignId}/send` is an alias. `?dryRun=true` returns `{ dryRun: true, campaignId, wouldLaunch: true }` with no writes. operationId: launchCampaign x-mint: href: /agents/reference/launch-campaign parameters: - $ref: '#/components/parameters/IdempotencyKey' - in: query name: dryRun schema: { type: boolean, default: false } responses: '200': description: The transition result — or the dryRun report. content: application/json: schema: oneOf: - $ref: '#/components/schemas/CampaignTransition' - type: object required: [dryRun, campaignId, wouldLaunch] properties: dryRun: { type: boolean, const: true } campaignId: { type: string, format: uuid } wouldLaunch: { type: boolean, const: true } '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/send: parameters: - $ref: '#/components/parameters/CampaignId' post: tags: [Campaigns] summary: 'Alias of POST /campaigns/{campaignId}/launch' description: | Alias of `POST /campaigns/{campaignId}/launch` — activates the campaign and runs the launch pipeline. (Before the campaigns refactor this was a batch "mark sequences ready" op; it is now the campaign-activate launch.) Same body, params, and result shape. operationId: sendCampaign x-mint: href: /agents/reference/send-campaign parameters: - $ref: '#/components/parameters/IdempotencyKey' - in: query name: dryRun schema: { type: boolean, default: false } responses: '200': description: The transition result — or the dryRun report. content: application/json: schema: oneOf: - $ref: '#/components/schemas/CampaignTransition' - type: object required: [dryRun, campaignId, wouldLaunch] properties: dryRun: { type: boolean, const: true } campaignId: { type: string, format: uuid } wouldLaunch: { type: boolean, const: true } '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/pause: parameters: - $ref: '#/components/parameters/CampaignId' post: tags: [Campaigns] summary: Pause a campaign (idempotent; ?dryRun=true) description: | Pauses the campaign. Idempotent — pausing an already-paused campaign is a no-op. `?dryRun=true` returns `{ dryRun: true, campaignId, wouldPause }` with no writes; a real pause returns the transition result carrying the `pause` facts. operationId: pauseCampaignById x-mint: href: /agents/reference/pause-campaign parameters: - $ref: '#/components/parameters/IdempotencyKey' - in: query name: dryRun schema: { type: boolean, default: false } responses: '200': description: The transition result — or the dryRun report. content: application/json: schema: oneOf: - $ref: '#/components/schemas/CampaignTransition' - type: object required: [dryRun, campaignId, wouldPause] properties: dryRun: { type: boolean, const: true } campaignId: { type: string, format: uuid } wouldPause: { type: boolean } '404': { description: CAMPAIGN_NOT_FOUND. } /campaigns/{campaignId}/resume: parameters: - $ref: '#/components/parameters/CampaignId' post: tags: [Campaigns] summary: Resume a campaign (idempotent; ?dryRun=true) description: | Resumes the campaign from where its sequences left off (the same `active` transition as launch; resume vs fresh-launch facts are derived from prior state). Idempotent. `?dryRun=true` returns `{ dryRun: true, campaignId, wouldResume }` with no writes; a real resume returns the transition result carrying the `resume` facts. operationId: resumeCampaignById x-mint: href: /agents/reference/resume-campaign parameters: - $ref: '#/components/parameters/IdempotencyKey' - in: query name: dryRun schema: { type: boolean, default: false } responses: '200': description: The transition result — or the dryRun report. content: application/json: schema: oneOf: - $ref: '#/components/schemas/CampaignTransition' - type: object required: [dryRun, campaignId, wouldResume] properties: dryRun: { type: boolean, const: true } campaignId: { type: string, format: uuid } wouldResume: { type: boolean } '404': { description: CAMPAIGN_NOT_FOUND. } # ── Scheduled agents segment ───────────────────────────────────── /scheduled-agents: get: tags: [Scheduled agents] summary: List scheduled agents (cursor-paginated; filters workspaceId/enabled) operationId: listScheduledAgents x-mint: href: /agents/reference/list-scheduled-agents parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: workspaceId schema: { type: string, format: uuid } - in: query name: enabled schema: { type: boolean } responses: '200': description: A page of scheduled agents (list envelope + `total`). content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items, total] properties: items: type: array items: { $ref: '#/components/schemas/ScheduledAgent' } total: type: integer description: Total scheduled agents in the listing's scope (all pages). post: tags: [Scheduled agents] summary: Create a scheduled agent (disabled by default) description: Requires workspaceId/name/prompt/cron. Invalid cron → INVALID_CRON / CRON_TOO_FREQUENT. operationId: createScheduledAgent x-mint: href: /agents/reference/create-scheduled-agent parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '201': description: The created scheduled agent. content: application/json: schema: { $ref: '#/components/schemas/ScheduledAgent' } /scheduled-agents/{id}: parameters: - $ref: '#/components/parameters/AgentId' get: tags: [Scheduled agents] summary: Fetch a scheduled agent (+ planBlocked, last run) operationId: getScheduledAgent x-mint: href: /agents/reference/get-scheduled-agent responses: '200': description: The scheduled agent. content: application/json: schema: { $ref: '#/components/schemas/ScheduledAgent' } '404': { description: SCHEDULED_AGENT_NOT_FOUND. } patch: tags: [Scheduled agents] summary: Edit a scheduled agent operationId: patchScheduledAgent x-mint: href: /agents/reference/update-scheduled-agent responses: '200': description: The updated scheduled agent. content: application/json: schema: { $ref: '#/components/schemas/ScheduledAgent' } delete: tags: [Scheduled agents] summary: Delete a scheduled agent (soft-delete) operationId: deleteScheduledAgent x-mint: href: /agents/reference/delete-scheduled-agent responses: '200': { description: '{ deleted: true }.' } /scheduled-agents/{id}/enable: parameters: - $ref: '#/components/parameters/AgentId' post: tags: [Scheduled agents] summary: Enable a scheduled agent operationId: enableScheduledAgent x-mint: href: /agents/reference/enable-scheduled-agent parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: The updated scheduled agent. content: application/json: schema: { $ref: '#/components/schemas/ScheduledAgent' } /scheduled-agents/{id}/disable: parameters: - $ref: '#/components/parameters/AgentId' post: tags: [Scheduled agents] summary: Disable a scheduled agent operationId: disableScheduledAgent x-mint: href: /agents/reference/disable-scheduled-agent parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: The updated scheduled agent. content: application/json: schema: { $ref: '#/components/schemas/ScheduledAgent' } /scheduled-agents/{id}/trigger: parameters: - $ref: '#/components/parameters/AgentId' post: tags: [Scheduled agents] summary: Manually trigger a run operationId: triggerScheduledAgent x-mint: href: /agents/reference/trigger-scheduled-agent parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '202': { description: '{ runId }.' } '402': { description: SUBSCRIPTION_REQUIRED (upgrade-plan handoff). } '409': { description: RUN_ALREADY_ACTIVE. } /scheduled-agents/{id}/runs: parameters: - $ref: '#/components/parameters/AgentId' get: tags: [Scheduled agents] summary: Run history (failed runs carry a workspace-chat handoff) description: | Run history newest-first, in the canonical list envelope. Not paginated — `nextCursor` is always `null`. operationId: listScheduledAgentRuns x-mint: href: /agents/reference/list-scheduled-agent-runs responses: '200': description: Run history in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { type: object, additionalProperties: true } # ── Projects segment (tenancy) ─────────────────────────────────── /projects: get: tags: [Projects] summary: List projects (cursor-paginated; ?search=) description: | Projects are managed **from the parent**: every `/projects/*` endpoint acts on the API key's own (parent) org and ignores the `x-origami-project` header. Newest first; a parent with no projects gets an empty page — "you have no projects" is not an error. operationId: listProjects x-mint: href: /agents/reference/list-projects parameters: - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' - in: query name: search description: Case-insensitive substring match on the project name. schema: { type: string } responses: '200': description: A page of projects in the canonical list envelope. content: application/json: schema: allOf: - $ref: '#/components/schemas/ListEnvelope' - type: object required: [items] properties: items: type: array items: { $ref: '#/components/schemas/Project' } post: tags: [Projects] summary: Create a project (child org) description: | Creates a project under the API key's parent org. `monthlyCredits` is the optional per-project budget cap in credits — `null` or omitted means uncapped. A child org's key cannot create projects (nesting is one level deep) → `400 PARENT_REQUIRED`. operationId: createProject x-mint: href: /agents/reference/create-project parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, minLength: 1, maxLength: 80 } monthlyCredits: type: integer minimum: 1 nullable: true description: Monthly budget cap in credits; null/omitted = uncapped. responses: '201': description: The created project. content: application/json: schema: { $ref: '#/components/schemas/Project' } '400': description: "VALIDATION_ERROR — bad body; or PARENT_REQUIRED — a child org's key cannot create projects." /projects/{projectId}: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [Projects] summary: Fetch a project operationId: getProject x-mint: href: /agents/reference/get-project responses: '200': description: The project object. content: application/json: schema: { $ref: '#/components/schemas/Project' } '404': $ref: '#/components/responses/ProjectNotFound' patch: tags: [Projects] summary: Update a project's name / budget cap description: | Partial update. For `monthlyCredits`, a number **sets** the cap, an explicit `null` **clears** it, and omitting the field leaves it alone. operationId: updateProject x-mint: href: /agents/reference/update-project requestBody: required: true content: application/json: schema: type: object properties: name: { type: string, minLength: 1, maxLength: 80 } monthlyCredits: type: integer minimum: 1 nullable: true description: Number sets the cap, explicit null clears it, omitted leaves it alone. responses: '200': description: The updated project. content: application/json: schema: { $ref: '#/components/schemas/Project' } '400': { description: VALIDATION_ERROR. } '404': $ref: '#/components/responses/ProjectNotFound' delete: tags: [Projects] summary: Delete a project (two-step; ?confirm=true, ?dryRun=true) description: | Deletes the project (child org) and cascades across its whole entity tree. Follows the v2 deletes convention — deliberately two-step: 1. Without `?confirm=true` (or with `?dryRun=true`) → HTTP 200 with the impact preview `{ id, name, confirmationRequired: true, willDelete: { workspaces, tables, rows } }`; nothing removed. 2. With `?confirm=true` → deletes and returns `{ id, name, deleted: true }`. operationId: deleteProject x-mint: href: /agents/reference/delete-project parameters: - in: query name: confirm description: Must be `true` to actually delete. Omit for the impact preview. schema: { type: boolean, default: false } - in: query name: dryRun description: Force the impact preview even when confirm is set. schema: { type: boolean, default: false } responses: '200': description: The impact preview (unconfirmed / dryRun) or the delete receipt (confirmed). content: application/json: schema: oneOf: - $ref: '#/components/schemas/ProjectDeletePreview' - type: object required: [id, name, deleted] properties: id: { type: string, format: uuid } name: { type: string } deleted: { type: boolean, const: true } '404': $ref: '#/components/responses/ProjectNotFound' # ── Account segment ────────────────────────────────────────────── /account: get: tags: [Account] summary: Org account overview (plan, capabilities, workspace usage) operationId: getAccount x-mint: href: /agents/reference/get-account responses: '200': description: The account overview. content: application/json: schema: { $ref: '#/components/schemas/Account' } /account/credits: get: tags: [Account] summary: Credit balance (supersedes v1 GET /credits) operationId: getAccountCredits x-mint: href: /agents/reference/get-credits responses: '200': description: The credits object. content: application/json: schema: { $ref: '#/components/schemas/Credits' } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: og_live_... parameters: AgentId: in: path name: id required: true schema: { type: string, format: uuid } RunId: in: path name: runId required: true schema: type: string pattern: '^[A-Za-z0-9_\\-]{1,128}$' description: | Run ids are stable text (not UUID) sourced from `chat_stream.id`. v2-issued ids carry a `v2_run-` prefix. Use the literal value returned in prior responses. WorkspaceId: in: path name: workspaceId required: true schema: { type: string, format: uuid } DocumentId: in: path name: documentId required: true schema: { type: string, format: uuid } TableId: in: path name: tableId required: true schema: { type: string, format: uuid } RowId: in: path name: rowId required: true schema: { type: string, format: uuid } ColumnId: in: path name: columnId required: true schema: { type: string, format: uuid } SequenceId: in: path name: sequenceId required: true schema: { type: string, format: uuid } BatchId: in: path name: batchId required: true schema: { type: string, format: uuid } EnrichmentRunId: in: path name: runId required: true description: The enrichment-run id (also its legacy `batchId`). schema: { type: string, format: uuid } CampaignId: in: path name: campaignId required: true description: The campaign id (a first-class `campaigns` row). schema: { type: string, format: uuid } ProjectId: in: path name: projectId required: true description: The project (child org) id. schema: { type: string, format: uuid } OrigamiProject: in: header name: x-origami-project required: false description: | Project (child org) selector — accepted on **every** v2 request except `/projects/*` (where it is ignored; see the Tenancy section in the API description). Present → the request is scoped to that project under the API key's parent org; absent → the request acts on the parent. Malformed id → `400 VALIDATION_ERROR`; unknown / cross-parent / deleted → `404 PROJECT_NOT_FOUND`. Plan gate and rate limits stay on the parent; billing draws on the parent wallet, subject to the project's optional budget cap. schema: { type: string, format: uuid } Cursor: in: query name: cursor required: false description: | Opaque pagination cursor. Pass the `nextCursor` from the previous page to fetch the next one; omit for the first page. schema: { type: string } Limit: in: query name: limit required: false description: | Max objects to return per page. Default 50, max 100 (the rows endpoint allows up to 200 and documents its own `limit`). schema: { type: integer, minimum: 1, maximum: 100, default: 50 } IdempotencyKey: in: header name: Idempotency-Key required: false description: | Transport-level replay protection — any v2 `POST` may send it (1–255 printable ASCII characters; UUIDs recommended). The first response for a key is recorded and replayed verbatim for retries with the same key + method/path/body for 24 hours; replayed responses carry the `Idempotency-Replay: true` response header. Reusing a key with a *different* request — or retrying while the original is still in flight — returns `409 IDEMPOTENCY_ERROR`. 5xx responses are never recorded (the retry re-executes). schema: type: string minLength: 1 maxLength: 255 pattern: '^[\x21-\x7e]{1,255}$' headers: IdempotencyReplay: description: | Present (literal `true`) when this response was replayed from a previously recorded `Idempotency-Key` result rather than executed. schema: { type: string, enum: ['true'] } schemas: ListEnvelope: type: object description: | The canonical v2 list envelope. Every list endpoint returns this shape; per-endpoint `items` element types (and documented extras like `total`) are declared on each operation. required: [object, items, nextCursor, url] properties: object: { type: string, const: list } items: type: array items: {} description: The page of objects. Element type is per-endpoint. nextCursor: type: string nullable: true description: Cursor for the next page; `null` means this is the last page. url: type: string description: The path this list was fetched from (query string excluded). CreateAgentRequest: type: object required: [prompt] properties: name: type: string maxLength: 80 description: Optional human-readable label. Auto-generated from prompt if absent. prompt: type: string minLength: 1 maxLength: 10000 workspaceId: type: string format: uuid nullable: true focusTableIds: type: array items: { type: string, format: uuid } maxItems: 50 default: [] description: | Table ids to focus the agent on. When the caller also supplies `workspaceId`, every id must belong to that workspace. When `workspaceId` is omitted, the workspace is inferred from the first id. attachments: type: array maxItems: 20 default: [] items: { $ref: '#/components/schemas/UploadedRunAttachment' } description: | Bind existing resources to the run. A `document` is injected as an attached file the agent reads with `ctx.loadFile(path)` (its current path is re-resolved at run start); a `table` is merged into `focusTableIds`. Every attachment must belong to the run's workspace + org, else `400 INVALID_ATTACHMENT` before admission. model: type: string enum: [origami-lite, origami-max] description: | Public model id. Plan-aware default — highest model unlocked on the caller's plan (`origami-lite` for `starter`, `origami-max` for `pro`+). Resolved server-side to an internal chat-agent model; the response's `request.model` echoes the public id (`origami-lite` or `origami-max`), never an internal name. Legacy aliases `origami-fast` and `origami-mid` are still accepted and normalized to `origami-lite`. UploadedRunAttachment: oneOf: - type: object required: [kind, documentId] properties: kind: { type: string, enum: [document] } documentId: { type: string, format: uuid } - type: object required: [kind, tableId] properties: kind: { type: string, enum: [table] } tableId: { type: string, format: uuid } description: A document (injected as an attached file) or a table (merged into focusTableIds). SendRunRequest: type: object required: [prompt] properties: prompt: { type: string, minLength: 1, maxLength: 10000 } focusTableIds: type: array items: { type: string, format: uuid } maxItems: 50 default: [] attachments: type: array maxItems: 20 default: [] items: { $ref: '#/components/schemas/UploadedRunAttachment' } description: See CreateAgentRequest.attachments. model: type: string enum: [origami-lite, origami-max] Workspace: type: object required: [object, id, name, createdByApi, url, createdAt] properties: object: { type: string, const: workspace } id: { type: string, format: uuid } name: { type: string } createdByApi: type: boolean description: True when the workspace was created through the API (upload-first bootstrap or agent auto-create). url: type: string format: uri description: Deep link a human can open. createdAt: { type: string, format: date-time } WorkspaceDeletePreview: type: object required: [workspaceId, name, confirmationRequired, willDelete] description: | Impact preview returned by `DELETE /workspaces/{workspaceId}` when `?confirm=true` is absent. Nothing has been removed. properties: workspaceId: { type: string, format: uuid } name: { type: string } confirmationRequired: { type: boolean, const: true } willDelete: type: object required: [tables, rows] properties: tables: { type: integer } rows: { type: integer } DocumentSummary: type: object required: [object, id, documentId, filename, vfsPath, kind, status, createdAt, updatedAt] properties: object: { type: string, const: document } id: { type: string, format: uuid } documentId: type: string format: uuid description: Legacy alias of `id` — kept for pre-restructure consumers. filename: type: string description: Last segment of the VFS path — the user-facing filename. vfsPath: { type: string } kind: { type: string } status: { type: string } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } DocumentDetail: allOf: - $ref: '#/components/schemas/DocumentSummary' - type: object required: [content] properties: content: { type: string } UploadFilesRequest: type: object required: [files] properties: files: type: array minItems: 1 description: Per-request file cap enforced (over it → 413 TOO_MANY_FILES). items: type: object required: [filename, content] properties: filename: { type: string, minLength: 1, maxLength: 255 } content: type: string description: Base64-encoded file bytes. mode: type: string enum: [table, append, document] description: | `table` (CSV → new table; the default for `.csv`), `append` (CSV → existing table; requires `tableId`), or `document` (force document storage). tableId: type: string format: uuid description: Required when mode is `append` — the table to append CSV rows to. Row: type: object required: [object, id, cells] properties: object: { type: string, const: row } id: { type: string, format: uuid } cells: type: object additionalProperties: true description: | Typed cells keyed by column slug. Each cell is polymorphic: `scalar` (input columns), `value` (+ run metadata where present), or `sequence` (a reference to the drafted sequence). EnrichmentRun: type: object required: [object, id, batchId, tableId, type, status, createdAt, completedAt] description: | A tracked batch of column-over-row work on a table (upserts, file ingests). `id` is canonical; `batchId` is the kept legacy alias (identical value). properties: object: { type: string, const: enrichment_run } id: { type: string, format: uuid } batchId: type: string format: uuid description: Legacy alias of `id` — kept for pre-restructure consumers. tableId: { type: string, format: uuid } type: type: string description: The run's work type (e.g. `upsert`). status: { type: string } createdAt: { type: string, format: date-time } completedAt: { type: string, format: date-time, nullable: true } EnrichmentRunDetail: allOf: - $ref: '#/components/schemas/EnrichmentRun' - type: object required: [rowCount, enrichments, creditsUsed] properties: rowCount: type: integer description: | Rows this run processed. For `upsert` runs this is the per-row outcome ledger length (one entry per input row). enrichments: type: object required: [total, completed, pending, failed] properties: total: { type: integer } completed: { type: integer } pending: { type: integer } failed: { type: integer } creditsUsed: type: integer description: Settled credit cost. `0` until the run reaches a terminal status. outcomes: type: array description: Present only on `upsert`-type runs — the per-row outcome ledger. items: type: object required: [inputIndex, rowId, outcome] properties: inputIndex: { type: integer } rowId: { type: string, format: uuid, nullable: true } outcome: { type: string, enum: [inserted, updated, skipped] } outcomeCounts: type: object description: Present only on `upsert`-type runs. required: [inserted, updated, skipped] properties: inserted: { type: integer } updated: { type: integer } skipped: { type: integer } UpsertReceipt: type: object required: [object, id, batchId, counts] description: | Response of `POST /tables/{tableId}/rows/upsert` and `.../rows/upsert-file` — an `enrichment_run` reference the caller polls via `GET /enrichment-runs/{runId}`. properties: object: { type: string, const: enrichment_run } id: { type: string, format: uuid } batchId: type: string format: uuid description: Legacy alias of `id`. counts: type: object required: [inserted, updated, skipped] properties: inserted: { type: integer } updated: { type: integer } skipped: { type: integer } Campaign: type: object required: [object, id, slug, name, status, workspaceId, tableId, channels, settings, brief, outOfLeads, createdAt, updatedAt] description: | A first-class `campaigns` row — org-global, homed in a workspace. Its queue is the set of sequences stamped with its id (one sequence per person). It owns no table: `tableId` is derived (informational) from the dominant table of its enrolled sequences. properties: object: { type: string, const: campaign } id: { type: string, format: uuid } slug: { type: string, nullable: true } name: { type: string } status: type: string enum: [draft, active, paused] description: The campaign's lifecycle state. workspaceId: { type: string, format: uuid } tableId: type: string format: uuid nullable: true description: Derived leads table (dominant enrolled-sequence table); null for a shell campaign. channels: type: object required: [email, linkedin] description: Which channels the campaign sends on. properties: email: { type: boolean } linkedin: { type: boolean } settings: type: object required: [blockActiveDuplicates, blockPriorContacts, autoTopUpEnabled] description: Auto-cancel toggles + auto-refill, surfaced read-only. properties: blockActiveDuplicates: { type: boolean } blockPriorContacts: { type: boolean } autoTopUpEnabled: { type: boolean } brief: description: Agent-authored brief (audience prose + estimated size); null before drafting. nullable: true outOfLeads: { type: boolean } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } CampaignSummary: type: object required: [object, id, slug, name, status, peopleCount] description: | List-summary of a campaign (returned by the campaign list endpoints). properties: object: { type: string, const: campaign } id: { type: string, format: uuid, nullable: true } slug: { type: string, nullable: true } name: { type: string } status: { type: string, enum: [draft, active, paused] } peopleCount: type: integer description: Live enrolled sequences (people) — non-deleted, non-cancelled. CampaignPerson: type: object required: [object, sequenceId, rowId, recipient, sendStatus, stopReason, profile, fitExplanation, fitScore, addedAt] description: | One person (enrolled sequence) in a campaign — the People-tab row shape. Returned by `GET /campaigns/{campaignId}/people` and its `/sequences` synonym. properties: object: { type: string, const: campaign_person } sequenceId: { type: string, format: uuid } rowId: { type: string, format: uuid, nullable: true } recipient: { type: string, nullable: true } sendStatus: type: string description: Aggregate send-status pill (draft / scheduled / sent / replied / …). stopReason: { type: string, nullable: true } profile: nullable: true description: Agent-injected identity (name / title / company / linkedin / website). fitExplanation: type: string nullable: true description: Agent-authored "why this person fits" write-up (markdown). fitScore: type: integer nullable: true description: 0–100 fit score of the source row, or null. addedAt: { type: string, format: date-time, nullable: true } CampaignStats: type: object required: [object, found, contacted, hasEmail, hasLinkedin, connectAccepted, connectSent, connectionRate, replied, replyRate] description: Headline engagement stats for a campaign. properties: object: { type: string, const: campaign_stats } found: { type: integer, description: People enrolled in the campaign. } contacted: { type: integer, description: People actually reached (≥1 sent/accepted message). } hasEmail: { type: boolean } hasLinkedin: { type: boolean } connectAccepted: { type: integer } connectSent: { type: integer } connectionRate: { type: number } replied: { type: integer } replyRate: { type: number } CampaignTransition: type: object required: [object, id, status, launched] description: | Result of a deterministic lifecycle transition (`launch` / `send` / `pause` / `resume`). Carries the transition facts in exactly one of `launch` / `pause` / `resume` depending on which transition ran. properties: object: { type: string, const: campaign } id: { type: string, format: uuid } status: { type: string, enum: [draft, active, paused] } launched: type: integer description: Drafts scheduled by this transition (0 unless launching/resuming). launch: type: object description: Present on a launch/resume that scheduled work. required: [scheduled, firstScheduledAt, missingRecipientCount, duplicateActiveCancelledCount, duplicatePriorCancelledCount] properties: scheduled: { type: integer } firstScheduledAt: { type: string, format: date-time, nullable: true } missingRecipientCount: { type: integer } duplicateActiveCancelledCount: { type: integer } duplicatePriorCancelledCount: { type: integer } blocked: type: object required: [reason, message, missingChannels] properties: reason: { type: string } message: { type: string } missingChannels: type: array items: { type: string } pause: type: object description: Present on a pause. required: [stoppedSequences, haltedSteps, inFlightSending, alreadyPaused] properties: stoppedSequences: { type: integer } haltedSteps: { type: integer } inFlightSending: { type: integer } alreadyPaused: { type: boolean } resume: type: object description: Present on a resume. required: [resumedSequences, noAccountSequences, missingChannels] properties: resumedSequences: { type: integer } noAccountSequences: { type: integer } missingChannels: type: array items: { type: string } CampaignDeletePreview: type: object required: [id, name, confirmationRequired, status] description: | Impact preview returned by `DELETE /campaigns/{campaignId}` when `?confirm=true` is absent (or `?dryRun=true` is set). Nothing has been removed. properties: id: { type: string, format: uuid } name: { type: string } confirmationRequired: { type: boolean, const: true } status: { type: string, enum: [draft, active, paused] } Project: type: object required: [object, id, name, monthlyCredits, usage, createdAt] description: | A project is a **child org** under the parent (agency) org — a customer's own isolated set of workspaces/tables while credits and the concurrency pool stay shared at the parent. Select it on any non-`/projects` request via the `x-origami-project` header. properties: object: { type: string, const: project } id: { type: string, format: uuid } name: { type: string } monthlyCredits: type: number nullable: true description: Monthly budget cap in credits, or `null` when uncapped. usage: type: object required: [spent, reserved] description: | This billing period's settled spend plus live in-flight reservations attributed to the project, in credits — the same numbers the in-app Teams page shows. properties: spent: type: number description: Settled charges this billing period, in credits. reserved: type: number description: Live reservations held by in-flight work right now, in credits. createdAt: { type: string, format: date-time } ProjectDeletePreview: type: object required: [id, name, confirmationRequired, willDelete] description: | Impact preview returned by `DELETE /projects/{projectId}` when `?confirm=true` is absent (or `?dryRun=true` is set). Nothing has been removed. properties: id: { type: string, format: uuid } name: { type: string } confirmationRequired: { type: boolean, const: true } willDelete: type: object required: [workspaces, tables, rows] properties: workspaces: { type: integer } tables: { type: integer } rows: { type: integer } SequenceSummary: type: object required: [object, id, sequenceId, campaignId, status, stopReason, sendStatus, tableId, columnId, rowId, workspaceId, url] properties: object: { type: string, const: sequence } id: { type: string, format: uuid } sequenceId: type: string format: uuid description: Legacy alias of `id` — kept for pre-restructure consumers. campaignId: type: string format: uuid nullable: true description: The first-class campaigns-row id this thread is enrolled in — follow it into `GET /campaigns/{campaignId}`. Distinct from `columnId` (the outreach column); null for legacy campaign-less sequences. status: { type: string } stopReason: { type: string, nullable: true } sendStatus: { type: string, nullable: true } tableId: { type: string, format: uuid, nullable: true } columnId: { type: string, format: uuid, nullable: true } rowId: { type: string, format: uuid, nullable: true } workspaceId: { type: string, format: uuid, nullable: true } url: type: string format: uri nullable: true description: Deep link a human can open. SequenceDetail: allOf: - $ref: '#/components/schemas/SequenceSummary' - type: object required: [steps] properties: steps: type: array items: { $ref: '#/components/schemas/SequenceStep' } SequenceStep: type: object required: [object, id, stepId, channel, kind, status, subject, body, recipient, scheduledAt, sentAt] properties: object: { type: string, const: step } id: { type: string, format: uuid } stepId: type: string format: uuid description: Legacy alias of `id` — kept for pre-restructure consumers. channel: { type: string } kind: type: string description: '`message` or `connect_request` (LinkedIn invites have no subject).' status: { type: string } subject: { type: string, nullable: true } body: { type: string, nullable: true } recipient: { type: string, nullable: true } scheduledAt: { type: string, format: date-time, nullable: true } sentAt: { type: string, format: date-time, nullable: true } ScheduledAgent: type: object required: [object, id, workspaceId, slug, name, description, prompt, cron, model, enabled, planBlocked, nextRunAt, lastRunAt, url] properties: object: { type: string, const: scheduled_agent } id: { type: string, format: uuid } workspaceId: { type: string, format: uuid } slug: { type: string } name: { type: string } description: { type: string, nullable: true } prompt: { type: string } cron: { type: string } model: { type: string, nullable: true } enabled: { type: boolean } planBlocked: type: boolean description: True when the org cannot currently run scheduled tasks (gated at dispatch). nextRunAt: { type: string, format: date-time, nullable: true } lastRunAt: { type: string, format: date-time, nullable: true } url: { type: string, format: uri } handoff: type: object description: Present when planBlocked — points the caller at the upgrade flow. Account: type: object required: [object, organization, plan, capabilities, workspaces] properties: object: { type: string, const: account } organization: type: object required: [id, name] properties: id: { type: string, format: uuid } name: { type: string, nullable: true } plan: type: object required: [id, name] properties: id: { type: string } name: { type: string } capabilities: type: object required: [canUseApi, canRunScheduledTasks, concurrentAgents] properties: canUseApi: { type: boolean } canRunScheduledTasks: { type: boolean } concurrentAgents: type: integer nullable: true description: null = unlimited. workspaces: type: object required: [used, limit] properties: used: { type: integer } limit: type: integer nullable: true description: null = unlimited. Credits: type: object required: [object, balance, currency] properties: object: { type: string, const: credits } balance: { type: number } currency: { type: string, const: credits } AgentSummary: type: object required: [object, id, name, workspaceId, createdAt] properties: object: { type: string, const: agent } id: { type: string, format: uuid } name: { type: string } workspaceId: { type: string, format: uuid } createdAt: { type: string, format: date-time } Agent: allOf: - $ref: '#/components/schemas/AgentSummary' - type: object required: [apiKeyOwned, lastRun] properties: apiKeyOwned: type: boolean description: | True iff this agent's `chat_session` row has a non-null `created_by_api_key_id` — i.e. it was created by the API and is owned by the caller's org. Always `true` on v2 read paths today; surfaced for forward compatibility. lastRun: nullable: true allOf: - $ref: '#/components/schemas/RunSummary' RunStatus: type: string enum: [running, completed, needs_input, step_cap_hit, incomplete, cancelled, errored, timed_out] description: | Single discriminator for the run's lifecycle. `needs_input` means the run completed cleanly but the agent's last assistant message included an `ask-questions` tool call; answer by sending a follow-up run on the same agent. `incomplete` means the model finished the step but the AI SDK could not parse a tool call it tried to emit (or it hit the output-token cap mid-tool-call), so the multi-step loop ended without the work being done — recoverable by sending a follow-up run on the same agent. Deploy restarts are auto-followed server-side, so callers never observe a `superseded` value. RunSummary: type: object required: [object, id, agentId, status, prompt, model, steps, startedAt] properties: object: { type: string, const: run } id: { type: string } agentId: { type: string, format: uuid } status: { $ref: '#/components/schemas/RunStatus' } prompt: type: string description: | The user prompt that drove this run, truncated to 200 characters with an ellipsis when longer. Suitable for a run-history list item; for the full prompt use Get Run. model: type: string enum: [origami-lite, origami-max] description: | Public model id the run actually executed on. When the run has no assistant message yet (still admitting), falls back to the plan default. steps: { $ref: '#/components/schemas/RunSteps' } startedAt: { type: string, format: date-time } completedAt: { type: string, format: date-time, nullable: true } RunSteps: type: object required: [completed, max] description: | Agent step progress. `completed` is the number of multi-step loop iterations the agent has actually executed; `max` is the plan-determined hard cap. Once `completed === max`, the run's `status` becomes `step_cap_hit`. properties: completed: { type: integer, minimum: 0 } max: { type: integer, minimum: 1 } RunRequest: type: object required: [prompt, model, focusTableIds] properties: prompt: { type: string } model: type: string enum: [origami-lite, origami-max] description: | Public model id — `origami-lite` or `origami-max`. Echoes the caller's request (after legacy-alias normalization) or the plan default when the request omitted `model`. Internal chat-agent ids (e.g. `origami-lobotomized`) are never surfaced here. focusTableIds: type: array items: { type: string, format: uuid } description: Echoed verbatim from the request. RunResponse: type: object required: [text, actions, tables, transcript, transcriptTruncated] description: | `null` while `status === "running"`. Once terminal, an object with the agent's cleaned prose, the structured workspace mutations it performed (`actions[]`), the full TableObjects for every touched table (`tables[]`), and (optionally) the full message transcript. `tables[]` is the resolved version of `actions[].tableId` — same shape as `GET /api/v2/tables/{id}`, fanned out server-side. Pass `?include=stats` on the Get Run call to attach economics on each embedded TableObject. properties: text: type: string nullable: true description: | Cleaned user-facing assistant text — internal markup (``, ``, ``, history-tool markers) is stripped server-side, identical to the stripping in `transcript` content. `null` when `status` is `errored` or `timed_out` (we suppress partial prose on failed runs). actions: type: array items: { $ref: '#/components/schemas/RunAction' } description: | Structured workspace-mutation actions the agent performed, in the order they fired. Projected from the persisted `data-*` parts the chat-agent's execute-code event listener writes onto each assistant message — the same audit trail the desktop UI renders. Empty when the run did no workspace mutations. tables: type: array items: { $ref: '#/components/schemas/Table' } description: | Full TableObjects for every table this run touched (derived from `actions[].tableId` plus any `focusTableIds` the caller passed). Same shape as a single `GET /api/v2/tables/{id}` response. Empty when the run touched no tables. transcriptTruncated: type: boolean description: | True when `transcript` was truncated to its hard cap. Always present (defaults to `false`) so consumers don't have to do a presence-check on every poll. transcript: type: array items: { $ref: '#/components/schemas/TranscriptMessage' } nullable: true description: | Full message history projected onto the surface a desktop user with **dev mode off** would see. Specifically: - Assistant text is stripped of internal markup (``, ``, ``, history-tool markers). - Tool calls are limited to the interactive widgets a non-dev user actually sees: `ask-questions`, `present-plan`, `contact-support`, `suggest-document-write`. The code the agent ran (`execute-code`), the VFS reads (`list-dir`, `read-file`, `grep`), and operational tools (`flag-session`) are dropped along with their results. - `reasoning` parts (dev-only in the UI) are dropped. - Empty assistant bubbles (text that was entirely internal markup and no public tool calls) are dropped. Opt-in only via `?include=transcript` on `GET /agents/{id}/runs/{runId}`. Default is `null`. RunTodo: type: object required: [pendingQuestions, nextActions] properties: pendingQuestions: type: array items: { $ref: '#/components/schemas/PendingQuestion' } description: | Structured questions extracted from the agent's most recent `ask-questions` tool call, if any. When non-empty AND the chat_stream completed cleanly, `status` is promoted to `"needs_input"`. Answer by sending a follow-up run on the same agent (`POST /agents/{id}/runs`) with the user's answer as `prompt` — any free-text string is accepted; the agent parses it as natural language. nextActions: type: array items: { $ref: '#/components/schemas/NextAction' } description: | Structured next-action suggestions extracted from the agent's `...` block, if any. Surface the `label` to the user; act on `type` (when present) as a typed CTA the host can fire directly. Run: allOf: - $ref: '#/components/schemas/RunSummary' - type: object required: [workspaceId, request, response, todo] properties: workspaceId: { type: string, format: uuid } request: { $ref: '#/components/schemas/RunRequest' } response: nullable: true allOf: - $ref: '#/components/schemas/RunResponse' todo: { $ref: '#/components/schemas/RunTodo' } NextAction: type: object required: [label] properties: label: type: string description: Natural-language label the agent suggested. type: type: string enum: [upload_csv, find_more_leads, export_csv, input_required, send_messages, schedule_play] description: | Optional typed CTA the host can act on directly. When absent, treat as a plain text suggestion the user can ask the agent to do next. tableSlug: type: string description: | Optional table reference for table-scoped types (e.g. `export_csv`). RunAction: type: object required: [type, tableId] description: | A single workspace-mutation event the agent performed. Discriminated by `type`; `tableId` is always present. Other fields are type-specific. v2 wire vocabulary speaks "leads", not "rows": the row-mutation action types are `leads_added`, `leads_deleted`, `leads_restored`. properties: type: type: string enum: - table_created - column_added - columns_updated - columns_deleted - leads_added - leads_deleted - leads_restored - columns_restored - table_restored tableId: type: string format: uuid tableName: type: string nullable: true description: Present on `table_created` and `table_restored`. columnId: type: string format: uuid nullable: true description: Present on `column_added`. columnName: type: string nullable: true description: Present on `column_added`. count: type: integer description: | Present on `columns_deleted`, `leads_deleted`, `leads_restored`, `columns_restored`. Number of items affected. leadCount: type: integer nullable: true description: Present on `leads_added`. Number of leads inserted. deletedAt: type: string format: date-time nullable: true description: Present on `columns_deleted` and `leads_deleted`. PendingQuestion: type: object required: [type, question, suggestedAnswers, freeformOption] properties: type: type: string enum: [single-choice, confirm] description: | Question type the agent emitted. `single-choice` lists options the user is expected to pick from; `confirm` is a single approval prompt. question: type: string description: Natural-language question text to surface to the user. suggestedAnswers: type: array items: { type: string } description: | Answers the agent proposed, in the order it listed them. For `single-choice` these are the agent's options; for `confirm` it is the single-entry array `["Confirm"]`. This is NOT a hard menu — the caller may always reply with any free-text via the follow-up run (see `freeformOption`). The field is here so a host LLM can bias its follow-up `prompt` toward what the agent expected. freeformOption: type: string description: | A stable, agent-agnostic CTA string clients can render as a final "your own answer" button next to `suggestedAnswers`. Always the literal `"Or something else"` in v2.0. example: Or something else ColumnKind: type: string enum: [input, enrichment, score, sequence] description: | Sequence-aware API classification of a column: * `input` — user-entered (`static`) column; the only kind writable via `POST /tables/{tableId}/rows/upsert`. * `enrichment` — `code` column that runs per row to fetch/compute a value. * `score` — relevance / fit-score column. * `sequence` — `code` column that drafts an outbound sequence (`ctx.upsertSequence(...)` / `.draftMessage(...)`, or one that already has sequences). List its sequences via `GET /tables/{tableId}/sequences`. Created via the agent or column code only — never via the upsert API. WorkspaceColumn: type: object required: [object, id, name, slug, kind, autoTrigger] description: Column metadata returned by `GET /tables/{tableId}/columns`. properties: object: { type: string, const: column } id: { type: string, format: uuid } name: { type: string } slug: type: string nullable: true description: Stable url-safe slug, or null when the column has none. kind: $ref: '#/components/schemas/ColumnKind' autoTrigger: type: boolean description: True when the column runs automatically on new leads. TableColumn: type: object required: [object, id, name, type, kind, slug, autoTrigger, credits, cells] properties: object: { type: string, const: column } id: { type: string, format: uuid } name: { type: string } type: type: string description: | Raw DB column type. Common values: `static` (user-entered), `code` (user-TS that runs per row, includes enrichments, scoring, and sequence columns), plus other internal-typed kinds for future extensibility. Treat as an open string. Prefer `kind` for classification — it splits `code` columns into `enrichment`, `score`, and `sequence`. kind: $ref: '#/components/schemas/ColumnKind' description: | Sequence-aware classification of this column. `sequence` columns draft outbound sequences — find them here rather than inspecting `type` (they share the `code` DB type with enrichments). Sequence columns are created via the agent or column code, never set through the upsert API. slug: type: string nullable: true description: Stable url-safe slug, or null when the column has none. autoTrigger: type: boolean description: | True when the column runs automatically on new leads. False for columns that only run when explicitly invoked (e.g. exports, manual-trigger code columns). credits: $ref: '#/components/schemas/CreditsLifetime' description: | Per-column historical cost. The table-level `credits.lifetimeUsed` is the sum of every column's `credits.lifetimeUsed` — the per-column breakdown is the primary surface for "which column is driving cost?". cells: $ref: '#/components/schemas/CellsLiveness' description: | Per-column liveness. `running > 0` means cells in this column are still being processed; the agent should not conclude "data not found" on empty cells until `running === 0`. stats: $ref: '#/components/schemas/ColumnStats' description: Present only when caller passes `?include=stats`. qualification: $ref: '#/components/schemas/ColumnQualification' description: | Present only when caller passes `?include=stats` AND this column has a relevance condition (i.e. acts as a quality filter). CellsLiveness: type: object required: [running, errored] description: | Snapshot of cell-pipeline activity. The v2 run-finished signal is independent of the cell-pipeline-finished signal — when a run completes, downstream cells often still enrich in the background. Surfacing these counts lets the agent distinguish "still working on it" from "tried and failed", which is the difference between waiting and reporting a negative result. properties: running: type: integer minimum: 0 description: | Number of cells currently being processed (a `cell_run` is in `waiting`, `queued`, or `running` for that cell). `running > 0` means the agent should NOT report "no data found" — wait and re-read. errored: type: integer minimum: 0 description: | Number of cells that hit a settled failure. Includes `errored`, `out_of_credits`, `subscription_required`, and `connection_required`. The user-driven `stopped` state is intentionally excluded — it's a cancellation, not a failure to surface. CreditsLifetime: type: object required: [lifetimeUsed] properties: lifetimeUsed: type: integer description: | Sum of all settled cell-run charges, in credits. Stable and additive — once a row enrichment finishes it never moves backwards. Reservations (in-flight cell_runs) are not included. ColumnStats: type: object required: [avgCreditsPerRun, callRate, totalRuns] properties: avgCreditsPerRun: type: number description: Average credits charged when this column actually runs. callRate: type: number minimum: 0 maximum: 1 description: Fraction of leads that triggered this column (0-1). totalRuns: type: integer description: Lifetime count of cell runs feeding the average. ColumnQualification: type: object required: [pass, fail, unsure, total] properties: pass: { type: integer } fail: { type: integer } unsure: { type: integer } total: { type: integer } Table: type: object required: [object, id, workspaceId, name, leadCount, columns, credits, cells, url, createdAt, updatedAt] properties: object: { type: string, const: table } id: { type: string, format: uuid } workspaceId: { type: string, format: uuid } name: { type: string } leadCount: type: integer description: | Non-deleted row count. v2 wire vocabulary speaks "leads" instead of "rows"; the underlying DB column is still `rows`. columns: type: array items: { $ref: '#/components/schemas/TableColumn' } credits: $ref: '#/components/schemas/CreditsLifetime' description: | Lifetime credit cost across every cell. Equals the sum of every `columns[].credits.lifetimeUsed`. cells: $ref: '#/components/schemas/CellsLiveness' description: | Table-level liveness rollup — sums every `columns[].cells`. `running > 0` means at least one cell on the table is still being processed (e.g., enrichment kicked off by a recent run hasn't fully settled). The calling agent should treat this as "still working", not "no data found". url: type: string format: uri description: Deep link a human can open. For programmatic row reads, use `GET /api/v2/tables/{tableId}/rows`. createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } stats: $ref: '#/components/schemas/TableStats' description: Present only when caller passes `?include=stats`. TableStats: type: object required: - creditsPerLead - creditsPerQualifiedLead - findMoreEstimatedCredits - qualification - funnel - running - leadSources - hasUnknownTotalWithMore description: | Forward-looking table economics — the same numbers the desktop UI shows at the top of the table. Only present on responses that opted in via `?include=stats`. properties: creditsPerLead: type: number description: Estimated credits per sourced lead (pre-qualification). creditsPerQualifiedLead: type: number description: | Estimated credits per qualified lead, including the loss from dedup, exclusion, and filter cascade. findMoreEstimatedCredits: type: number description: Per-execution cost of the "Find More" code, in credits. qualification: $ref: '#/components/schemas/TableQualificationStats' nullable: true description: | Null when the table has no relevance conditions configured. funnel: { $ref: '#/components/schemas/TableFunnelStats' } running: { $ref: '#/components/schemas/TableRunningStats' } leadSources: type: array items: { $ref: '#/components/schemas/TableLeadSourceStats' } hasUnknownTotalWithMore: type: boolean description: | True when ≥1 active lead source has unknown TAM AND `has_more=true` (e.g. Twitter / LinkedIn post search). Consumers should render "X found (more available)" instead of a numeric TAM when set. TableQualificationStats: type: object required: - rate - fetchRate - qualifiedLeads - effectiveLeadsPerQualified - estimatedQualifiedLeads properties: rate: type: number nullable: true minimum: 0 maximum: 1 fetchRate: type: number nullable: true minimum: 0 maximum: 1 description: Filter-only qualification rate (excludes dedup / exclusion). qualifiedLeads: { type: integer } effectiveLeadsPerQualified: type: number description: Empirical leads-processed per qualified lead. estimatedQualifiedLeads: type: integer nullable: true TableFunnelStats: type: object required: - totalSourcedLeads - postStaticLeads - dedupedLeads - dedupRate - excludedLeads - excludedRate properties: totalSourcedLeads: { type: integer } postStaticLeads: type: integer description: Leads surviving dedup + exclusion + required static filters. dedupedLeads: { type: integer } dedupRate: type: number nullable: true minimum: 0 maximum: 1 excludedLeads: { type: integer } excludedRate: type: number nullable: true minimum: 0 maximum: 1 TableRunningStats: type: object required: [runningLeads, oldestActiveRunStartedAt] properties: runningLeads: type: integer description: Leads with active (queued / running / waiting) cell runs. oldestActiveRunStartedAt: type: string format: date-time nullable: true TableLeadSourceStats: type: object required: [id, name, avgCreditsPerLead, totalCredits, totalLeads, source] properties: id: { type: string, format: uuid } name: { type: string } avgCreditsPerLead: { type: number } totalCredits: { type: number } totalLeads: { type: integer } source: type: string enum: [historical, estimate] description: | `historical` when the average is derived from real cost_events; `estimate` when it was computed by static regex analysis of the lead source's TypeScript code. TranscriptMessage: type: object properties: role: type: string enum: [user, assistant, tool] content: { type: string } toolCalls: type: array items: type: object properties: id: { type: string } name: { type: string } input: {} toolCallId: { type: string } name: { type: string } result: { type: string } truncated: { type: boolean } ErrorEnvelope: type: object required: [error, code] properties: error: { type: string } code: { type: string } details: type: object additionalProperties: true handoff: type: object description: | Present on 4xx errors the caller's user can resolve in-app — a forwardable link on the canonical app origin. required: [kind, url, label] properties: kind: { type: string } url: { type: string, format: uri } label: { type: string } responses: Unauthorized: description: Missing or invalid API key content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Missing API key code: UNAUTHORIZED ValidationError: description: | Bad request body or semantic validation failure. Route-level codes include `VALIDATION_ERROR`, `MODEL_NOT_AVAILABLE`, `UNKNOWN_WORKSPACE`, `UNKNOWN_TABLE`, and `WORKSPACE_TABLE_MISMATCH`. content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Invalid request body code: VALIDATION_ERROR details: issues: - path: prompt message: Required InsufficientCredits: description: | Org out of credits at admission time. `402` can also be `SUBSCRIPTION_REQUIRED` when the plan does not include API access; both shapes use the same `{ error, code }` envelope. content: application/json: schema: type: object # Two distinct 402 shapes share this slot: INSUFFICIENT_CREDITS # carries the credit fields below; SUBSCRIPTION_REQUIRED (plan # lacks API access) carries only { error, code, hint }. Only # `error` + `code` are common to both, so the credit fields are # optional here. required: [error, code] properties: error: { type: string } code: { type: string, enum: [INSUFFICIENT_CREDITS, SUBSCRIPTION_REQUIRED] } creditsRequired: { type: integer } creditsAvailable: { type: integer } topUpUrl: { type: string } message: { type: string } hint: { type: string } example: error: Insufficient credits code: INSUFFICIENT_CREDITS creditsRequired: 5 creditsAvailable: 2 topUpUrl: https://origami.chat/settings/billing?source=api message: | Origami needs at least 5 credits to start any run; you have 2. Top up: https://origami.chat/settings/billing?source=api AgentBusy: description: A run is already in flight on this agent content: application/json: schema: type: object properties: error: { type: string } code: { type: string, enum: [AGENT_BUSY] } currentRun: type: object properties: id: { type: string } startedAt: { type: string, format: date-time } ConcurrentLimit: description: Org-wide concurrent-agent cap reached (shared with the Origami UI). headers: Retry-After: schema: { type: integer } description: Seconds. Median wall-clock of the last 200 terminal runs in this org (fallback 60). content: application/json: schema: type: object properties: error: { type: string } code: { type: string, enum: [CONCURRENT_LIMIT_EXCEEDED] } limit: { type: integer } activeSessions: type: array items: type: object properties: agentId: { type: string } name: { type: string } startedAt: { type: string, format: date-time } canUpgrade: { type: boolean } AgentNotFound: description: Agent not found (or belongs to another org) content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Agent not found code: AGENT_NOT_FOUND RunNotFound: description: Run not found content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Run not found code: RUN_NOT_FOUND ProjectNotFound: description: Project not found (unknown, cross-parent, or deleted — no existence leak) content: application/json: schema: allOf: - $ref: '#/components/schemas/ErrorEnvelope' - example: error: Project not found code: PROJECT_NOT_FOUND